Changelog
a r.uby.dev project
What's next
v15.0.3
Changes since v15.0.2.
This release fixes ActiveRecord :json/:jsonb serialization so tool
call arguments round-trip as JSON objects instead of arrays of pairs.
Fix
- activerecord: serialize tool call arguments properly
Fix a bug where the ActiveRecord:json/:jsonblayer serialized tool call arguments as an array of pairs instead of a Hash. The context is now serialized through its JSON form, so tool call arguments round-trip as JSON objects that providers accept.
v15.0.2
Changes since v15.0.1.
This release fixes a concurrency race in MCP tool calls by
reference-counting the transport session, so overlapping tool calls
reuse the running transport instead of racing start/stop.
Fix
- mcp: fix concurrent MCP tool call race
LLM::MCPnow reference-counts its transport session: the first caller starts the transport and the last caller stops it. Concurrent or overlapping tool calls reuse the running transport instead of racingstart/stop, avoiding "MCP transport is not running" errors that could occur with theasyncstrategy. An externally started transport is never stopped by a borrower.
v15.0.1
Changes since v15.0.0.
This release fixes agent set handling of single Symbol/Proc
values and resolves ORM options as Symbols through the bound record
instead of the LLM::Agent instance.
Agent
- agent: fix
set (skills|tools): Symbol|Proc
Theskills,tools,confirm, andschemaclass accessors now consistently resolve a singleSymbolorProclazily at agent initialization, soLLM::Agent.set(skills: proc { [...] })andLLM::Agent.set(tools: :tools)no longer raise. Thesetfamily shares onesingle_callable?helper rather than each accessor duplicating its own logic.
Fix
- orm: resolve a
Symbolthrough the bound record
When an ORM model usingacts_as_agent(ActiveRecord),plugin :agent(Sequel), oracts_as_llm/plugin :llmconfigures an option as aSymbol, that symbol is now resolved on the model instance (or its bound record) instead of theLLM::Agentinstance. The contexts and agents built by the wrappers are now bound to the record, so a model likeagent.set :toolswith atoolsmethod on the record works as expected.
v15.0.0
Changes since v14.0.0.
This release renames usage to token_usage across contexts, agents,
and messages, makes context_window return nil when unknown, and
makes LLM::Cost accessors always return Float. It also adds the
Alibaba provider, automatic API key discovery from the environment,
new context-usage and context-used methods, a retry budget for
rate-limited requests, and a range of REPL and skills improvements.
Breaking
Migration
| Old | New |
|---|---|
ctx.usage / agent.usage / msg.usage |
ctx.token_usage / agent.token_usage / msg.token_usage (usage remains an alias) |
LLM::Message#usage returns LLM::Object |
LLM::Message#token_usage returns a copy of LLM::Usage, for assistant messages only |
ctx.context_window returns 0 when the model isn't in the registry |
returns nil when unknown |
LLM::Cost#input (and other accessors) return nil when unused |
return 0.0 |
ctx.usage returns the most recent assistant message usage |
sums token usage across all assistant messages |
a skill exposes its tool as weather (the skill name) |
the generated tool is now named weather-skill |
rename
#usageto#token_usageacross contexts, agents, and messages
LLM::Context#usage,LLM::Agent#usage, andLLM::Message#usageare now aliases oftoken_usage.LLM::Message#token_usagenow returns a copy ofLLM::Usageinstead ofLLM::Object, and only returns a value for assistant messages.LLM::Context#context_windownow returnsnilwhen unknown
LLM::Context#context_windownow returnsnilwhen the model's context window size is not known to the runtime, instead of0. This makes the code check for a window instead of a number, so an unknown window no longer reads as a real (zero) size.LLM::Costaccessors always returnFloatobjects
The cost accessors onLLM::Cost(input,output,input_audio,output_audio,input_image,cache_read,cache_write, andreasoning) now always return aFloat, returning0.0when no tokens of that kind were used, instead ofnil. Callers can sum and compare cost values without guarding againstnil.expose new context methods on the ActiveRecord and Sequel wrappers
Theacts_as_llm(ActiveRecord) andplugin :llm(Sequel) wrappers now exposecontext_usedandcontext_usage, delegating to the wrappedLLM::Context.token_usagereplacesusage(which remains as an alias), andcontext_windownow returnsnilwhen the model's context window is unknown instead of0.
Core
discover API keys from the environment
Cloud provider factories (LLM.anthropic,LLM.google,LLM.deepseek,LLM.openai,LLM.xai,LLM.mistral,LLM.zai,LLM.moonshot,LLM.alibaba, andLLM.aliyun) now resolve the provider's API key automatically when nokey:is given, by walking the environment variable names listed in the models.dev registry. SoLLM.openaiworks without an explicit key as long asOPENAI_API_KEY(or one of the registry's alternative names) is set in the environment. A missing key raisesArgumentError.cli: auto-discover credentials and support Bedrock
bin/llm.rbnow resolves the provider through theLLMfactory methods instead of mapping environment variable names directly, so it picks up Bedrock (all three AWS credentials) and relies on the same automatic key discovery as the library. The CLI also always starts now: without arguments it falls back toollamaorllamacpp. A provider whose credentials are not set exits with status 1.cli: add
-cand-nswitches
bin/llm.rbnow accepts a-c STRATEGYswitch to choose the concurrency strategy used for tool calls (thread,async,fork, or any of the other strategies) and a-n TRANSPORTswitch to choose the HTTP transport (net-http,net-http-persistent, orcurb), both forwarded to the session's agent and provider.context: keep runtime parameters from reaching the provider
LLM::Contextnow strips its runtime-only parameters (guard,retry_budget,concurrency,transformer, andcompactor) before merging params into a provider request, so they can never cross the context-provider boundary and risk an API-level error.add
retry_budgetsupport for rate-limited requests
LLM::Contextnow accepts aretry_budget:that automatically sleeps and retries a rate-limited request up to the given number of times before raisingLLM::RateLimitError. Each retry sleeps a growing interval (2s, 4s, 6s, ...) and notifies the stream throughLLM::Stream#on_rate_limit.LLM::Agentenables a budget of 5 by default, while a raw context disables it (0) unless configured.add
LLM::Usage.zero
AddLLM::Usage.zeroas a zero-valued usage object.LLM::Context#usage,LLM::Agent#usage, and the ActiveRecord and Sequel wrappers now returnLLM::Usageobjects instead ofLLM::Objectwhen no provider usage has been recorded yet.add
LLM::Context#context_usageandLLM::Agent#context_usage
AddLLM::Context#context_usageandLLM::Agent#context_usage, which return the fraction of the model's context window currently used as aRational(for exampleRational(100, 10_000)), ornilwhen the used amount or the window size is unknown. The REPL status bar now renders this fraction instead of computing the remainder from raw token counts.add
LLM::Context#context_usedandLLM::Agent#context_used
AddLLM::Context#context_usedandLLM::Agent#context_used, which return the live context size (in tokens) of the most recent assistant message, ornilwhen no assistant message has a recorded token usage. This fills the gap left aftertoken_usagebecame accumulative and no longer represented a single turn, so callers can read how much of the context window has been used without walking the messages themselves.
Provider
add
LLM::Provider#registry
AddLLM::Provider#registry, which returns the provider's model registry.LLM::Context#registryandLLM::Agent#registrynow delegate to their underlying provider instead of looking it up on their own.add
LLM::Alibabafor Alibaba Cloud Model Studio
LLM::Alibabais a new provider that talks to Alibaba Cloud Model Studio through its OpenAI-compatible API, including the Qwen3 family of models. Create an instance withLLM.alibaba, also aliased asLLM.aliyun, which accepts the samekey:,host:, andbase_path:options as the OpenAI provider. The provider defaults to thedeepseek-v4-flash-0731model and supports chat completions, streaming, tool calls, and structured output through the shared OpenAI-compatible path; image, audio, moderation, responses, and vector store endpoints raiseNotImplementedError. Model metadata ships indata/alibaba.jsonfor the registry.alibaba: support structured outputs via
json_object
LLM::Alibabanow supports structured output through a sharedjson_objectfallback, since Alibaba models do not supportjson_schemanatively. The schema is described in an injected system message that also satisfies the "messages must contain the word json" requirement. The same shared fallback now also backs DeepSeek.alibaba: default to the pay-as-you-go host
The defaultLLM::Alibabahost is nowdashscope-intl.aliyuncs.com. Override it globally with theDASHSCOPE_API_HOSTenvironment variable, or per instance withLLM.alibaba(host: ...), for example to point at a Token Plan endpoint.alibaba: use
DASHSCOPE_API_KEYas the default key env var
LLM::Alibabanow discovers its API key fromDASHSCOPE_API_KEYinstead ofALIBABA_API_KEY, following the models.dev registry convention.alibaba: raise
LLM::InsufficientQuotaErrorfor exhausted quota
AddLLM::InsufficientQuotaError, a subclass ofLLM::RateLimitError, for when a provider reports a tokens-per-minute (TPM) quota limit.LLM::Alibabanow raises it when Alibaba responds with aninsufficient_quotaerror. Since it subclassesRateLimitError, quota errors are retried like other rate limits.bedrock: auto-discover AWS credentials from the environment
LLM.bedrocknow infers its credentials from theAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andAWS_REGIONenvironment variables when they are not passed explicitly, matching the other cloud providers. A missing key raisesArgumentError.add
LLM::Bedrock#key?
AddLLM::Bedrock#key?, which overrides the superclass method to check all three Bedrock credentials (access_key_id,secret_access_key, andregion) instead of a single API key.
Function
make
Sequential::Groupabide by theLLM::Function::Groupcontract
LLM::Function::Sequential::Groupnow receives an array ofLLM::Function::Sequential::Taskobjects instead of rawLLM::Functionobjects, matching the interface shared by every other concurrency strategy.LLM::Function::Array#taskwraps each function as aSequential::Taskbefore constructing the group, and the group delegatesspawn,alive?, andwaitto those tasks. This fixesSequential::Group#alive?, which always returnedfalse, and restores guard handling for sequential execution by honoring the sharedguarded:option onSequential::Task.function: redirect output streams in
:forktool processes
The:forkconcurrency strategy (viaLLM::Function::Fork::Task) now redirects the child process's$stdoutand$stderrtoFile::NULL, so a forked tool can no longer clobber the parent terminal, for example by blanking the curses REPL display. A tool that genuinely needs the terminal can still reopen/dev/tty; the file descriptor stays available to the child.
Fix
a2a: fix a typo in the HTTP transport
Fix a bug inLLM::A2A::Transport::HTTPwhere the constructor readuri.portinstead of@uri.port, which crashed the program wheneverLLM::A2A.restorLLM::A2A.jsonrpcwas used. The transport now reads the port from the parsed@uri.openai: report usage for streamed completions requests
Fix a bug in the OpenAI completions path whereparams[:stream]was checked after it had been deleted from the params hash, so the check always evaluated tofalse. The fix checks the resolved stream'senabled?instead, sostream_options: {include_usage: true}is added to streamed requests and API usage is reported back to the caller.curb: read the stream body and resolve streaming requests
Fix two bugs inLLM::Transport::Curbthat left thecurbtransport unusable. The request body setter now reads a streaming request's body stream into a string (dropping the chunked transfer header, which curb replaces with a content length), and the result builder now accumulates the response body from theon_bodycallback instead of leaving it empty.cli: handle errors in
main
Wrap all ofbin/llm.rb'smainmethod in error handling: an interrupted session exits gracefully withBye!, an explicit provider is passed the resolved transport, and any unexpected error prints a formatted diagnostic with a link to issue tracking before exiting.cli: persist the session mapping file
Fix a bug wherebin/llm.rbsaved the session file at~/.llm.rb/<provider>/<uuid>.jsonbut never wrote the updated working-directory mapping back to~/.llm.rb/<provider>.json. The mapping file is now written whenever a new session is registered.context: aggregate usage across all assistant messages
LLM::Context#usagenow sums token usage across every assistant message in the conversation instead of returning only the first message's usage.LLM::Cost.fromnow subtracts reasoning tokens from the output total and cache-read tokens from the input total before pricing, and prices reasoning tokens with the model's reasoning rate when one is available.
Repl
draw a top chrome row with the cwd and active model
The curses-based REPL now draws a white-on-blue row at the very top of the screen showing the current working directory on the left and the active model on the right. The row is drawn above the transcript and uses a new blue status-bar color pair.redraw the window on resize
The curses-based REPL now handles the terminal resize signal (KEY_RESIZE) while reading input, clearing and redrawing the entire window so the layout stays aligned after the terminal is resized.hide the cursor until the window is ready
Fix a visual glitch where the curses-based REPL showed the cursor at position 0,0 at startup and then jumped it to the input area once the window was drawn. The cursor is now hidden until the input field has been drawn and the cursor can be placed directly into it.collapse the cost to two decimal places
LLM::Cost#to_snow renders the total cost with two decimal places (for example$0.01), so the REPL status bar shows a compact cost estimate instead of a long run of digits.add auto-complete ability for commands
LLM::Repl::Commandsubclasses can now override acompletemethod to autocomplete their arguments. The method receives the command's parameters as keyword arguments, with the non-nil keyword being the active fragment, and returns candidate completions. Repeated TAB presses cycle through the candidate list.add
LLM::Repl#modelandLLM::Repl#model=
LLM::Replnow tracks the active model in its ownmodelattribute, seeded from the wrapped agent's model. The status bar reads the model through the repl instead of the agent, so the model can be switched within a session.add
/modelcommand
A new/model <name>command switches the active model within a single REPL session. Its argument auto-completes through the text-to-text models in the registry.repl: restrict autocomplete to text-to-text models
The/modelcommand's argument auto-complete now suggests only text-to-text models, so embedding and other non-chat models are left out of the completion list.repl: highlight GitHub-flavored codeblocks
The curses-based REPL now parses the GitHub-stylefences that models commonly emit as real code blocks. Kramdown's native fenced-code syntax uses `~~~`, so thefences were previously parsed as inline code spans. The language name is now shown in bold white above the code, which renders in green.repl: fix a scroll render artifact
Fix a bug where scrolling upward could leave a piece of text just above the status row as a render artifact. The row above the status row is now cleared on every buffer render.repl: add a buffer row below the blue status bar
The curses-based REPL buffer now starts with an empty row below the blue status bar, improving the visual spacing of the first exchange in the chat.repl: pin
cursesandkramdownto tested versions
The REPL now pinscursesto~> 1.6andkramdownto~> 2.5throughLLM.require, so it loads gem versions known to have been tested instead of whatever happens to be installed.
Skills
skills: append
-skillto the generated tool name
A skill is now exposed as a tool named"<skill>-skill"instead of just the skill's name, so a skill likeweatherthat also uses a tool namedweatherno longer collides with it (or a same-named global tool) in the tool registry.skills: add
LLM::Streamskill lifecycle callbacks
AddLLM::Stream#on_skill_callandLLM::Stream#on_skill_return, which are called before a skill's sub-agent runs and after it finishes.on_skill_returnreceives theLLM::Agentsub-agent that ran the skill along with the resultingLLM::Response, so a stream can inspect the sub-agent's conversation, tally its usage, or add a verification step. A stream can use the two callbacks to know when a skill sub-agent is running.
Registry
add
LLM::Registry::Modelas a comparable model wrapper
AddLLM::Registry::Model, a wrapper around a model's registry metadata (pricing, limits, capabilities, and modalities). Models are comparable by price, somodels.sortorders them from cheapest to most expensive. The class exposes predicate helpers such astool_call?,reasoning?,structured_output?,open_weights?,text?,image?,audio?,pdf?, andvideo?, plusinput_cost,output_cost, andcontext_windowaccessors.gemspec: bundle the deepdive guide from
docs/
The gemspec now packages the deepdive guide fromdocs/deepdive.mdanddocs/deepdive/*/*.mdafter the deepdive sources moved fromresources/todocs/, so the full guide ships with the gem.make
LLM::Registry#modelsreturn model objects
LLM::Registry#modelsnow returns a list ofLLM::Registry::Modelobjects instead of model name strings. Use the newLLM::Registry#keysmethod to get the model names.refresh DeepInfra model metadata
Updatedata/deepinfra.jsonwith current pricing for the DeepSeek V4, DeepSeek-V3, DeepSeek-R1-0528, and Kimi-K3 models, and markstructured_outputsupport for one model.
v14.0.0
Changes since v13.1.0.
This release replaces the transformer= setter with the new
LLM::Transformer class hierarchy, refactors guards into the
LLM::Guard superclass with per-tool-call interception, and replaces
the agent tool_attempts parameter with the tool_budget class DSL.
It also adds the Moonshot (Kimi) provider, the LLM::Tool.set
bulk-assignment DSL, a LLM::Function#return shorthand, and a wide
range of REPL improvements.
Breaking
Migration
| Old | New |
|---|---|
ctx.transformer = MyTransformer |
LLM::Context.new(transformer: MyTransformer) |
transformer.call(ctx, prompt, params) |
transformer.call(message:, **opts) |
~/.llm.rb/session.json (shared across providers) |
~/.llm.rb/<provider>/<uuid>.json (scoped per provider and directory) |
agent.talk(tool_attempts: 25) |
set :tool_budget => 50 (disabled by default) |
LLM::LoopGuard |
LLM::Guard::Loop |
guard: true / ctx.guard = MyGuard |
guard: MyGuard, guard_options: {} |
guard.call(ctx) (warning string) |
guard.call(function:) (LLM::Function::Return or nil) |
LLM::GuardError |
"guard_error" |
replace the transformer setter with
LLM::Transformer
The previoustransformer=setter and 3-argumentcall(ctx, prompt, params)interface onLLM::Contexthave been replaced by the newLLM::Transformerclass interface. Configure a transformer class throughtransformer:and options throughtransformer_options:instead.cli: scope session persistence per provider and directory
bin/llm.rbno longer shares a single session file between providers. Each provider now has a~/.llm.rb/<provider>.jsonfile that maps the current working directory to a UUID-scoped session file under~/.llm.rb/<provider>/<uuid>.json, so sessions are scoped to both the provider and the directory they were started in.cli: harden the executable against bad inputs
bin/llm.rbnow prints an error message followed by the help menu and exits with status 1 when the-pswitch is given without an argument or when an unknown option is passed. Previously unknown options produced a warning but the run continued. The session-file lookup also no longer rewrites~/.llm.rb/<provider>.jsonwhen it already exists.agent: replace
tool_attemptswith thetool_budgetclass DSL
LLM::Agentreplaces thetool_attemptsparameter with atool_budgetclass DSL (tool_budget { 50 }) that caps the number of tool calls allowed in a single turn. Once the budget is spent, the agent sends an in-band advisory message back through the model telling it to solve the problem with fewer tool calls.
The feature is now disabled by default; the oldtool_attemptsparameter defaulted to 25, which long-horizon agents could easily exhaust in a single turn.guard: replace
LLM::LoopGuardwith theLLM::Guardclass hierarchy
LLM::Guardis a new superclass for context-level supervisors, withLLM::Guard::Loop(replacingLLM::LoopGuard) andLLM::Guard::Nullas the built-in implementations.LLM::Contextnow acceptsguard:(a guard class defaulting toLLM::Guard::Null) andguard_options:(a hash forwarded to the guard'scallmethod), matching the transformer and compactor interfaces. The old boolean and hash forms ofguardand theguard=setter are removed.LLM::AgentenablesLLM::Guard::Loopby default.guard: block individual tool calls instead of the whole batch
LLM::Guard#callnow receives the pendingfunction:and returns anLLM::Function::Return(or nil) instead of a warning string for the entire batch, so a guard can block a single tool call while the rest of the batch still executes. Custom guards that implemented the oldcall(ctx)warning-string interface must be updated to return aLLM::Function::Returninstead.errors: drop
LLM::GuardError
RemoveLLM::GuardError. The constant was never raised as an exception; it only named the in-band error type for guarded tool returns. Guarded tool returns now use the string"guard_error"as their error type.
Core
add
LLM::Provider#build_messagesfor assembling outgoing messages
LLM::Provider#build_messagesnormalizes a prompt intoLLM::Messageobjects and prepends the existing history, replacing the per-providerbuild_complete_messagesimplementation. The method is idempotent: prompts that are alreadyLLM::Messageinstances or arrays of messages are returned as-is.copy the
paramshash inLLM::ContextandLLM::Agent
LLM::ContextandLLM::Agentnow copy theparamshash in their constructors before mutating it, leaving the caller's hash untouched. Previously the constructors deleted keys from the caller's hash in place.gemspec: ship the deepdive sub-files in the gem
The gemspec now includesresources/deepdive/*/*.mdin the gem package, so the full deepdive guide (fundamentals, advanced, protocols, and everything-else chapters) is available after installation.add short aliases to
LLM::Cost
LLM::Costnow offers short aliases for its cost accessors:input,output,input_audio,output_audio,input_image,cache_read,cache_write, andreasoning. Each alias matches the key used by#to_h, socost.inputreads the same value ascost.input_costs.
Provider
- add
LLM::Moonshotfor the Moonshot AI provider
LLM::Moonshotis a new provider that talks to Moonshot AI through its OpenAI-compatible Kimi API. Create an instance withLLM.moonshot, which accepts the samekey:,host:, andbase_path:options as the OpenAI provider. The provider defaults to thekimi-k3model and supports chat completions, streaming, tool calls, and structured output through the shared OpenAI-compatible path; image, audio, moderation, responses, and vector store endpoints raiseNotImplementedError. Model metadata ships indata/moonshot.jsonfor the registry.
Transformer
add
LLM::Transformerfor rewriting messages before they reach the provider
LLM::Transformeris a new superclass for message transformers. A transformer is bound to a context and rewrites a single message before it is sent to the provider, which makes it possible to redact personal information or rewrite any message before it goes out over the wire. Each subclass implementscall(message:, **opts)and returns the message to send, either by mutating it in place or returning a new one.LLM::Transformer::Nullis a no-op transformer used as the default.hook the transformer API into
LLM::Context
LLM::Contextnow acceptstransformer:(a transformer class defaulting toLLM::Transformer::Null) andtransformer_options:(a hash forwarded to the transformer'scallmethod). The transformer runs on the most recent message in both chat and responses turns.LLM::Stream#on_transformandLLM::Stream#on_transform_finishnow receive the transformer instance as their single argument.
Tool
- add
LLM::Tool.setfor bulk-assigning tool properties
LLM::Tool.setaccepts a hash ofname,description,parameters,required, anddefaultsto configure a tool subclass in a single call. Parameters are defined as tuples of[name, type, description, options], matching the same interface as the existingparameterDSL. Unknown keys raiseKeyError.
Function
- add
LLM::Function#returnfor building tool returns
LLM::Function#returnreturns anLLM::Function::Returnbuilt from the function's own id and name, using the given hash as its value. It is a shorthand mainly useful inside aLLM::Guardsubclass and is defined viadefine_methodbecausereturnis a Ruby keyword.
Guard
- run the guard for streamed tool calls
Fix a gap where the guard was not consulted when a tool call was queued while a response was still streaming. The guard is now stamped onto the functions a context binds, so it runs wherever a task is spawned, including tool calls queued from a stream. A blocked call yields itsguard_errorreturn without executing.
Agent
- add
LLM::Agent#compacted?
LLM::Agent#compacted?delegates to the wrappedLLM::Context#compacted?and reports whether the conversation has been compacted, so callers can detect when history was trimmed.
Change
- openai: default to
gpt-5.6-luna
The default OpenAI chat model has changed fromgpt-5.4-minitogpt-5.6-luna. The new model is OpenAI's fastest and most affordable option, matching the kind of default llm.rb aims for.
Repl
show an unknown context state after
/compact
After running/compact, the REPL status line now rendersContext compactedand the context-usage bar shows???instead of a percentage, because the used context is unknown until the next response.land on a blank line after Ctrl+N at the end of history
When recalling history with Ctrl+P and Ctrl+N, Ctrl+N at the last item now advances to a blank line so you can start typing new input, instead of staying stuck on the last item in history (the previous behavior). Recalling with Ctrl+P or Ctrl+N also no longer overwrites the input when there is no history to show.restore history wrap for Ctrl+P and Ctrl+N
Fix a regression where Ctrl+P and Ctrl+N recalled history text without reflowing it into rows, so recalled lines wider than the terminal were clipped. Recalled text now flows through the same word-wrap path as typed input and wraps at the terminal width.restore Ctrl+D deletion across rows
Fix a bug where Ctrl+D at the end of an input row was a no-op, so multiline input could not be joined by deleting a row break. Deleting at the end of a row now consumes the break and pulls the next row up, restoring the split space so merged words do not run together.center the buffer with 20% gutters
The curses-based REPL now centersLLM::Repl::Bufferin a content area that is 60% of the terminal width, with an unused 20% gutter on each side. The drawing area is based on the available rows and columns instead of a fixed 80-column width, andBuffer#wrapnow hard-breaks words that overflow the width onto the next row, fixing a bug where a word could be cut off between rows.apply markdown to previous messages
The curses-based REPL now renders every message in the buffer with markdown styling, including messages that were already present when the session started or restored from disk. Previously only newly streamed responses were styled; older messages fell back to plain text.add
LLM::Repl#senderfor the user label
LLM::Repl#senderreturns the label used for user messages in the curses-based REPL. It defaults to"You"(previously"user"), and the buffer layout now places each label on its own line followed by the message content and a blank line.add
LLM::Repl::Colorfor coloring the curses UI
AddLLM::Repl::Coloras a new module that returns Curses color bitmasks.Color.enableinitializes 8 color pairs, and methods likeColor.bluereturn the correspondingCurses.color_pair(X)bitmask, which can be bitwise OR'ed with other attributes such asCurses::A_BOLD. User labels in the REPL are now rendered in blue instead of plain bold text.split on words rather than characters
LLM::Repl::Buffer#wrapnow breaks text on word boundaries instead of wrapping one character at a time. A word that does not fit on the current row moves to the next, and only a single word longer than the whole width is hard-broken, so text is never clipped by the window.render kramdown typographic symbols and smart quotes
Fix a bug where certain character sequences such as...were not rendered at all in the curses-based REPL. Kramdown parses them into:typographic_symand:smart_quotenodes, which previously fell through to the children clause and were dropped. The markdown renderer now maps them to their unicode equivalents: ellipsis, en and em dashes, guillemets, and single and double quotation marks.apply colors to the markdown renderer
The curses-based REPL now renders markdown with theLLM::Repl::Colorpalette: headers and strong text in white, code spans and code blocks in green, and links in underlined green, on the black background. Previously markdown styling used bold, underline, and reverse video attributes only.wrap the input line at word boundaries
The curses-based REPL input line now wraps words whole onto the next row at the terminal width instead of cutting them in half. A word that does not fit on the current row moves to the next row, and only a single word longer than the whole width is hard-broken, so typed text is never clipped by the window.distinguish the connecting and thinking status bar phases
The curses-based REPL status bar now showsConnecting • Esc to cancelwhile the model is establishing a connection, then switches toThinking • Esc to cancelonce a tool call or text fragment arrives on the stream. Active tool calls appear in the status bar with a lambda indicator.add emoji to the status bar phases
The curses-based REPL status bar now uses emoji to identify each phase at a glance: a globe (🌐) while the model is connecting, and a brain (🧠) while it is thinking. The text after the emoji still readsConnecting • Esc to cancelandThinking • Esc to cancelrespectively.render the status bar with color and attributes
The curses-based REPL status bar now supports colored and attributed status text, so the lambda indicator for active tool calls is drawn in bold red.
Registry
- refresh model metadata across providers
Updatedata/*.jsonfiles with current provider model listings and pricing. Remove the deprecated Claude Opus 4.1 entries from the Anthropic registry, addQwen/Qwen3.8-Maxto DeepInfra, and add alowreasoning-effort option to DeepSeek.
v13.1.0
Changes since v13.0.0.
This release adds LLM::Agent class DSL attributes (path, description),
extends skills with file-path loading and the tools: all directive, adds new
built-in tools (LLM::Tool::Ruby, LLM::Tool::EditFile), introduces the
LLM::Tracer::PrettyLogger for human-readable tracing, renames Transcript
to Buffer across the REPL, ships a bin/llm.rb CLI entry point, and fixes
several agent and tool bugs around persistence, interruption, and naming.
Core
- add post install message with deepdive link
The gemspec now includes apost_install_messagethat points users to the deepdive guide athttps://r.uby.dev/llm/deepdiveafter installation, making it easier for new users to discover the project documentation.
Agent
add
descriptionclass DSL and instance method
LLM::Agentnow has adescriptionclass DSL (description "release engineer") and a corresponding#descriptioninstance method. The description is an optional self-documenting string that serves as a brief summary of the agent's purpose. It can be set via the class DSL,LLM::Agent.set(description: ...), orLLM::Agent.new(description: ...).add
pathclass DSL and instance method
LLM::Agentnow has apathclass DSL (path "contexts/admin.json") and a corresponding#pathinstance method. When a path is set, the agent automatically restores its conversation history from that file on initialization and saves it back after eachtalkoraskturn, making session persistence across process restarts transparent.
Skills
accept a path to a markdown file
LLM::Skill.loadnow accepts a path to a markdown file in addition to a directory path. When given a file path, the file is read directly instead of looking for aSKILL.mdinside a directory. This makes it possible to load a single markdown file as a skill without placing it in a dedicated directory.extend with
allkeyword for loading the full tool registry
LLM::Skillnow supportstools: all(ortools: "*") in the frontmatter to load all tools from the globalLLM::Tool.registry. Previously, thetools:frontmatter only acceptedinherit, an array of tool names, or nothing. The newallkeyword makes it possible to give a skill access to every registered tool without listing them individually.
Tools
add
LLM::Tool::Rubyfor executing Ruby code in a subprocess
LLM::Tool::Rubyis a new built-in tool that runs a string of Ruby code in a separate Ruby process with a configurable timeout (default 15s). The code runs in an isolated address space unaware of its parent, making it useful for safe(ish) dynamic code execution. It must be required explicitly withrequire "llm/tools/ruby"and requires thetest-cmd.rbgem.rename
LLM::Tool::SwapTexttoLLM::Tool::EditFile
TheSwapTexttool has been renamed toLLM::Tool::EditFileto better match the naming of sibling tools (ReadFile,WriteFile). The oldrequire "llm/tools/swap_text"path no longer exists; userequire "llm/tools/edit-file"instead.
Tracer
- add
LLM::Tracer::PrettyLoggerfor human-readable tracing
LLM::Tracer::PrettyLoggeris a new tracer that writes human-readable request and tool-call logs to a console or file. Unlike the structured JSON output ofLLM::Tracer::Logger, the pretty logger emits single-line entries with inline context, making it easier to follow agent activity at a glance. It writes to$stderrby default and accepts anio:option for file output.
Repl
rename
LLM::Repl::TranscripttoLLM::Repl::Buffer
LLM::Repl::Transcripthas been renamed toLLM::Repl::Bufferto better reflect its role as a conversation state manager. The oldstartandfinishmethods have been renamed toopenandcloserespectively. The public accessor onLLM::Replhas been renamed fromtranscripttobuffer.add
write_messagefor formatted message writing
LLM::Repl::Buffer#write_messageandLLM::Repl#write_messageprovide a convenience method that takes a username and content string, formatting the output with a bolduser:label and a trailing newline. This is simpler than the equivalent sequence ofwritecalls.add
Command#write_messageand refactorCommand#write
LLM::Command#write_messageprovides a convenience method that takes a username and content string, matching the same interface onLLM::ReplandLLM::Buffer. TheCommand#writemethod is now implemented on top ofwrite_message, always prefixing output withcommand(<name>):. Thewho:keyword argument previously accepted bywritehas been removed; usewrite_messageinstead.display pre-existing agent messages when the repl starts
WhenLLM::Agent#replstarts, any messages already in the agent's buffer are now rendered in the REPL window. Previously the REPL started with an empty transcript even when the agent carried prior conversation history, making it harder to resume a session. Tool-call and tool-return messages are skipped to avoid visual noise.
CLI
- add
bin/llm.rbfor launching the REPL from the command line
A new executable script (bin/llm.rb) provides a convenient way to start an interactive REPL session directly from the terminal. It auto-detects the provider from environment variables likeOPENAI_API_KEY, supports a-p PROVIDERflag for explicit provider selection, a-tflag for temporary (non-persistent) sessions, and-hfor help. Sessions are automatically saved to~/.llm.rb/by default.
Fix
agent: fix
pathrestore on first run
Fix a bug whereLLM::Agentcalled@ctx.restore(path:)even when the path's file did not exist. The fix checksFile.readable?(@path)before attempting to restore, so the agent starts with a blank conversation on first use instead of failing with a file-not-found error.tools: re-raise
LLM::Interruptto abort the turn
LLM::Tool::Git,LLM::Tool::Mkdir,LLM::Tool::Rg,LLM::Tool::Ruby, andLLM::Tool::Shellnow re-raiseLLM::Interruptafter killing their running command. The previous behavior rescued the interrupt and killed the child process but let the turn continue, which meant a cancelled tool call did not abort the conversation turn. Re-raising ensures the entire turn is interrupted.tools: rescue
LLM::Interruptin shell-based tools
LLM::Tool::Shell,LLM::Tool::Git,LLM::Tool::Mkdir, andLLM::Tool::Rgnow rescueLLM::Interruptand kill their running command, preventing orphaned child processes when a tool is interrupted during execution.agent: fix default name derivation
Fix a bug whereLLM::Agentused without a subclass derived its default name as"l-lm-agent"instead of"agent". The fix replaces the regex-based parameterization with a pattern that correctly handles single-word class names and multi-word namespaced names.function:
#paramsalways returns anLLM::Object
LLM::Function#paramsnow always returns anLLM::Objectrepresenting the function's parameter schema. Previously it returnednilwhen a function defined no parameters, forcing every caller to guard againstnil. All provider adapters now usefn.params.to_hinstead offn.params || {type: "object", properties: {}}.
v13.0.0
v13.0.0 is released under the MIT license. Commercial, personal, educational, and all other uses are permitted under the standard MIT terms.
Seven breaking changes. Concurrency strategies have been renamed
(:call → :sequential, :task → :async), spawn is now
task, and the :async strategy has been rebuilt from the ground
up. It no longer blocks and now supports interruption. The compactor
has been refactored into pluggable strategies. Interruption is now
reliable across all six concurrency backends. The functions and
functions? methods have been renamed to pending_functions and
pending_functions?.
Migration from v12.6.0
| Old | New |
|---|---|
fn.spawn(:call) |
fn.task(:sequential) |
fn.spawn(:task) |
fn.task(:async) |
ctx.wait(:call) |
ctx.wait(:sequential) |
agent.concurrency :task |
agent.concurrency :async |
LLM::Function::FiberGroup |
LLM::Function::Fiber::Group |
LLM::Function::CallGroup |
LLM::Function::Sequential::Group |
LLM::Function::TaskGroup |
LLM::Function::Async::Group |
Compactor.new(model:, token_threshold:) |
Compactor::Truncate.new(ctx) |
on_compaction(ctx, compactor) |
on_compaction(compactor) |
ctx.functions / ctx.functions? |
ctx.pending_functions / ctx.pending_functions? |
agent.functions / agent.functions? |
agent.pending_functions |
Breaking
rename
LLM::Function#spawnasLLM::Function#task
LLM::Function#task(previouslyspawn) now consistently returns aLLM::Function::Taskobject that can be spawned, waited on, and passed toLLM::Function::Group. The old implementation alternated between spawning immediately or returning a raw thread or fiber.rename concurrency strategies (
:call→:sequential,:task→:async)
The:callconcurrency strategy is now:sequential, and the:taskstrategy is now:async.LLM::Agent.concurrency,LLM::Context#wait,LLM::Function::Array#task, andLLM::Function#taskall accept the new names. The old names raiseArgumentError.rename group classes
Group classes have been moved into their strategy's namespace:FiberGroup→Fiber::Group,ThreadGroup→Thread::Group,CallGroup→Sequential::Group,TaskGroup→Async::Group,Fork::Group→Fork::Group,Ractor::Group→Ractor::Group.repurpose
LLM::Function::Taskas a task interface superclass
LLM::Function::Taskhas been repurposed from a general-purpose class that tried to support multiple concurrency strategies into an abstract base class that defines the task interface. Individual strategies (Sequential::Task,Thread::Task,Fiber::Task,Async::Task,Fork::Task,Ractor::Task) now subclass it and implementspawn,alive?,interrupt!, andwait.fix
:asyncconcurrency (now backed by a managedLLM::Function::Async::Reactoron a background thread)
The:asyncstrategy previously usedAsync {}which blocked the caller until all tasks completed and did not support interruption. The fix replaces it with a per-turnLLM::Function::Async::Reactoron a background thread. Work is submitted viasubmit(&block)and consumed by the reactor's event loop through a thread-safeQueue.Async::Groupmanages the reactor lifecycle and spawns tasks lazily onwait.
Interruption pushesLLM::Interruptto the task's result queue instead of usingFiber#raise, and results are bridged back to the caller through a secondQueue. This work drove the broader refactor of strategy naming, the spawn/wait split, and theTasksuperclass. The:asyncstrategy needed the same interface the other strategies already had.compactor: refactor to strategy-based interface
LLM::Compactorhas been refactored from a single class that performed LLM-based summarization into a strategy-based superclass. Each subclass implements a different compaction strategy viacall(**opts). The old summarization approach (usingmodel:,token_threshold:,message_threshold:, andretention_window:options) has been removed. The built-inLLM::Compactor::Truncatestrategy drops the oldest messages when the conversation exceeds a configured size.rename
LLM::Context#{functions,functions?}andLLM::Agent#{functions,functions?}
LLM::Context#functionsandLLM::Context#functions?have been renamed toLLM::Context#pending_functionsandLLM::Context#pending_functions?respectively. The same rename applies toLLM::Agent#functions(nowLLM::Agent#pending_functions). Thepending_functionsname was already available as an alias in v12.5.0; this change removes the oldfunctionsname entirely.
Core
- extend
LLM.requirewith an optional version argument
LLM.requirenow accepts a secondversionparameter that is passed toKernel#gembefore loading, enabling version constraints for optional runtime dependencies. For example,LLM.require "test-cmd.rb", "~> 1.1"ensures a minimum gem version is available. This is used internally by theGit,Rg,Mkdir, andShelltools to enforce compatibility with thetest-cmd.rbgem.
Compactor
add
Truncatestrategy for dropping oldest messages
LLM::Compactor::Truncateis a new built-in compaction strategy that drops the oldest messages when the conversation exceeds a configured size. It preserves tool call/return pairs so the algorithm never breaks in the middle of a sequence. Configured withkeep:(default 64), it emits the standardon_compactionandon_compaction_finishLLM::Streamlifecycle callbacks. No LLM call is made; the strategy is purely lossy but fast and requires no network.raise when given an unparseable
keep:value
LLM::Compactor::Truncatenow raisesArgumentErrorwhen thekeep:parameter cannot be parsed as an integer or percentage string, instead of failing with an obscure error later during execution.accept percentage string for the
keep:parameter
LLM::Compactor::Truncate#callnow accepts a percentage string such as"80%"for thekeep:parameter, which keeps approximately 80% of the most recent messages. Integer values continue to work as before. This makes it easy to trim proportionally rather than to an absolute number of messages.add
Nullstrategy for no-op compaction
LLM::Compactor::Nullis a new built-in compaction strategy that does nothing. It is used as the default compactor when no strategy is configured on a context, ensuring the compactor interface is always present without requiring a separate nil check.accept both
LLM::AgentandLLM::Context
LLM::Compactor#initializenow accepts bothLLM::AgentandLLM::Contextinstances. When given an agent, the internal context is unwrapped automatically, making the compactor API more flexible when working with agents.
Context integration
accept
compactorandcompactor_optionsparameters
LLM::Contextnow acceptscompactor:(a compactor class defaulting toLLM::Compactor::Null) andcompactor_options:(a hash of options forwarded to the compactor'scallmethod) parameters. The compactor is automatically invoked at the beginning of eachtalkturn. The previouscompactor=setter has been removed in favour of constructor-driven configuration.on_compactionandon_compaction_finishreceive a single argument
LLM::Stream#on_compactionandLLM::Stream#on_compaction_finishnow accept a single argument (the compactor instance) instead of two arguments (context and compactor). The context is still available viaLLM::Compactor#ctx, so access to the context is not lost. This simplifies the callback interface for compaction lifecycle observers.
Tools
add
LLM::Tool::Utilsmodule for shared command execution logic
A newLLM::Tool::Utilsmodule provides sharedwait(command:, timeout:)andnowhelper methods for tools that execute commands. Tools that includeUtilscan wait on a running command and automatically kill it when it exceeds the configured timeout, usingProcess.clock_gettimewithCLOCK_MONOTONICfor precise timing. The module is used by both theShellandRgtools internally.shell: add
timeoutparameter for command execution deadlines
TheLLM::Tool::Shelltool now accepts atimeoutparameter (default 60s) that automatically kills commands exceeding the specified time limit, preventing hung processes from blocking the agent indefinitely.rg: add
timeoutparameter for search execution deadlines
TheLLM::Tool::Rgtool now accepts atimeoutparameter (default 5s) that automatically kills search commands exceeding the specified time limit, preventing long-running searches from blocking the agent indefinitely.git: add
timeoutparameter for command execution deadlines
TheLLM::Tool::Gittool now accepts atimeoutparameter (default 5s) that automatically kills git commands exceeding the specified time limit, preventing hung processes from blocking the agent indefinitely.
Schema
- properties are now ordered and support indifferent access
LLM::Schema::Leaftracks property definition order in a newindexattribute, matching the convention already used byLLM::Command::Parameter. Internally,@propertiesis stored as anLLM::Objectinstead of a plainHash, so lookups with both string and symbol keys work.
Buffer
more array-like message management
LLM::Buffernow exposesfirst,reject!,select!,shift,clear,drop,take, andreverse, making it easier to query and mutateLLM::Context#messageslike an ordinary Array.reject!is aliased asdelete_iffor familiarity.last(nil)no longer returns the last message
LLM::Buffer#lastnow uses an internalUNDEFINEDsentinel to distinguish between no argument (lastreturns the last message) andnil(last(nil)is treated as an argument). Previouslynilwas indistinguishable from no argument.
Function
consolidate
callandcall!into one method
The privatecall!method has been merged into the publicLLM::Function#call. The separatecall!method existed for tracer-scoping logic now handled directly insidecall. All internal call sites now usefunction.callinstead offunction.call!.add
LLM::Function::Groupas an abstract base class
A new abstract base class (LLM::Function::Group) defines the interface that all concurrency strategy groups must implement:alive?,interrupt!, andwait. Each strategy group (Sequential::Group,Thread::Group,Fiber::Group,Async::Group,Fork::Group,Ractor::Group) now subclasses this base.split
spawnandwaitacross all strategies
spawnnow starts execution without blocking, andwaitcollects the result.on_tool_startmoved into each task'sspawnso the tracer span covers execution rather than construction. Each task and group now exposes a publicspawnmethod alongside the existingwait/valuemethods.spawn tasks lazily in
Group#wait
All concurrency strategy groups (Fiber::Group, Fork::Group, Ractor::Group, Thread::Group) now automatically spawn their tasks whenwaitis called if they haven't been spawned yet, matching the existingAsync::Groupbehavior. This makes the spawn/wait contract consistent across all six concurrency backends.
Agent
- add
nameclass DSL and instance method
LLM::Agentnow has anameclass DSL (name "admin") and a corresponding#nameinstance method. The name is resolved through the same lazy-resolution path as other agent attributes. It can be set viaLLM::Agent.set(name: ...),LLM::Agent.new(name: ...), or the class DSL. When no name is given, a default is derived from the class name (e.g.,SystemAdminbecomessystem-admin). The REPL uses the name as the prompt label and transcript prefix, making it easier to distinguish multiple sessions.
REPL
agent identity in the prompt
LLM::Agent#replandLLM::Repl.newaccept aname:parameter (defaulting toLLM::Agent#name) that sets the input prompt toprovider(name)>and labels transcript messages with the agent's name instead of a hardcodedagent:. Useful when running multiple sessions./compactcommand
New built-in/compactcommand frees context window space by dropping the oldest messages viaLLM::Compactor::Truncate. Supports both integer (/compact 32) and percentage (/compact 75%) arguments. Defaults to keeping the last 128 messages.tab-completion for
/commands
Pressing Tab on an input line starting with/autocompletes the command name. Repeated Tab presses cycle through matching commands. Powered byLLM::Command.complete(str)which is available outside the REPL too.command system enhancements
Commands can set parameter defaults in theircallmethod signature (e.g.,def call(n: 128)). Aliases like/quitnow inherit their parent's description and parameters. Commands also have access to the activeagentandreplvia public readers.tool argument sorting
Tool parameters in the status bar are now displayed in definition order (using the newindexattribute), regardless of the order the model returns them.expanded markdown rendering
The curses-based markdown renderer now handles lists (<ul>,<ol>), blockquotes, horizontal rules, hyperlinks (underline), images ([image: alt text]), and tables (aligned columns).input improvements
Ctrl+P and Ctrl+N walk through conversation history (user messages only, managed byLLM::Repl::Walker). Page Up/Down scroll the transcript by a page. ENTER and BACKSPACE are now mapped to raw character codes fromCurses.getchinstead ofCurses::Keyconstants.
Object
- preserve the original key name in
KeyErrormessages
LLM::Object#fetchnow preserves the original key name when a key is not found, instead of raisingKeyErrorwithkey not found: nil. The previous behavior occurred when the given key was not found in the stored hash, causing internal lookup to returnniland lose the original key reference.
Registry
- refresh model metadata across providers
Updatedata/*.jsonfiles with current provider model listings and pricing. Mark several DeepInfra models as deprecated (meta-llama/ Meta-Llama-3.1-8B-Instruct,Qwen/Qwen1.5-110B-Chat, andmistralai/Mixtral-8x7B-Instruct-v0.1). Correct xAI cache-read pricing from $0.50 to $0.30 per million input tokens.
Fix
agent: fix default name resolution when name is not explicitly set
Fix a bug whereLLM::Agentderived its default name fromself.classinstead ofself, causing the name to be"class"instead of a parameterized version of the actual class name (e.g.,"system-admin"forSystemAdmin). The fix usesselfdirectly, which correctly resolves the class name at the instance level.function: make Fork::Task and Ractor::Task inherit LLM::Function::Task
LLM::Function::Fork::TaskandLLM::Function::Ractor::Tasknow explicitly subclassLLM::Function::Taskand accept an options hash as their second argument, matching the constructor signature used by the other four task classes. The interface was already compatible but the inheritance was missing by mistake. It is now consistent across all six concurrency backends.google: fix
streamparameter leakage that broke the provider
Fix a bug in the Google provider wherestream: stream.enabled?was being merged into request parameters, causing API-level errors. The Google provider does not use astreamparameter. Streaming is controlled via the URL path (streamGenerateContentvsgenerateContent). The fix removes the leaked parameter and correctly routes streaming requests through the appropriate path.fork: fix deadlock on xchan.rb channel
Fix a deadlock in the:forkconcurrency strategy where both the writer and reader could get stuck on the xchan channel, preventing the reader from draining the channel. The deadlock surfaced as an errno failure, especially with large tool returns. The fix requires xchan.rb v0.22.0 and uses theSOCK_STREAMsocket type for communicating a tool's return value.
v12.6.0
Changes since v12.5.1.
This release adds bulk defaults for tools and agents: LLM::Tool.defaults
for setting parameter defaults and LLM::Agent.set for mass-assigning
class-level defaults, both mirrored on ActiveRecord and Sequel agent models.
It also makes LLM::Interrupt reliable across every concurrency strategy
(:thread, :call, :fiber, :task, :fork, and :ractor) so tool cancellation
works consistently regardless of execution backend, and fixes a stale fiber
reference in LLM::Context#talk that could prevent interruption after a
prior call.
Add
tool: add
defaultsmethod for setting parameter defaults
AddLLM::Tool.defaults(properties)for bulk-setting default values on tool parameters, matching the same interface asLLM::Schema.defaults. Each key maps to a parameter name; unknown keys raiseKeyError.agent: add
setmethod for bulk-assigning class-level defaults
AddLLM::Agent.set(properties)for mass-assigning agent defaults from a Hash. Each key maps to a class-level accessor; unknown keys raiseKeyError.active_record: expose
setonacts_as_agentmodels
ActiveRecord models usingacts_as_agentcan callsetto bulk-assign agent class-level defaults.sequel: expose
setonplugin :agentmodels
Sequel models usingplugin :agentcan callsetto bulk-assign agent class-level defaults.
Fix
function: raise
LLM::Interrupton thread where tool is running
On cancel,LLM::Interruptis now raised on the thread that is running a tool. The tool can rescueLLM::Interruptand gracefully terminate (e.g., clean up resources). The previous approach usedThread#interruptwhich was less reliable. It did not interrupt a sleeping thread.function: suppress thread exception reporting in
:threadconcurrency
Threads spawned by the:threadconcurrency strategy now havereport_on_exceptionset tofalse, preventing noisy exception messages from appearing on stderr when a thread is interrupted during tool execution.context: clear
@owneraftertalkcompletes
LLM::Context#talknow clears the@ownerreference in anensureblock after the method completes, sointerrupt!does not attempt to interrupt a stale fiber reference from a prior call.function: raise
LLM::Interrupton thread waiting inCallGroup#wait
When usingctx.wait(:call),LLM::Interruptis now raised on the thread executing the sequential tool wait.CallGroup#waittracks the active thread andinterrupt!raisesLLM::Interrupton it, enabling interruption of the:callconcurrency strategy just like the existing:threadstrategy.function: raise
LLM::Interrupton fiber-backed tool tasks
LLM::Interruptis now raised on the active fiber viaFiber#raisewhen interrupting:fiber-concurrency tools.
Task#interrupt!now dispatches by task type:Thread#raisefor threads,Fiber#raisefor fibers. Making interruption reliable across all concurrency strategies.function: raise
LLM::Interrupton fork-backed tool tasks
LLM::Interruptis now raised on the main thread of a fork child process viaThread.main.raise(LLM::Interrupt)when interrupting:fork-concurrency tools, and the forkTask#waitre-raises the interrupt on the parent side. Making interruption reliable across all concurrency strategies including:fork.function: raise
LLM::InterruptonAsync::Task-backed tool tasks
LLM::Interruptis now raised on the underlying fiber of anAsync::TaskviaFiber#raisewhen interrupting:task-concurrency tools.Task#interrupt!now detectsAsync::Taskinstances and dispatches toFiber#raise, extending reliable interruption to the:taskconcurrency strategy under the Async runtime.function: raise
LLM::Interrupton ractor-backed tool tasks
LLM::Interruptis now raised on the main thread inside a ractor viaThread.main.raise(LLM::Interrupt)when interrupting:ractor-concurrency tools. A listener thread inside the tool ractor waits for an interrupt message viaRactor.receiveand raisesLLM::Interrupton the ractor's main thread.
Task#interrupt!delegates to the mailbox to send the interrupt message. Extending reliable interruption to the:ractorconcurrency strategy.
v12.5.1
Changes since v12.5.0.
This release reverts the global LLM::Function registry fallback for tool
resolution that was added in v12.5.0.
Change
- function: remove the global registry fallback for tool resolution
Remove theLLM::Function.find_by_namefallback that was added in v12.5.0 as an intermediate step between available-tools lookup and raisingLLM::NoSuchToolError. Tool calls not found in the available tools list now go directly tofunction_missing(which raisesLLM::NoSuchToolError) without first checking the globalLLM::Functionregistry.
v12.5.0
Changes since v12.4.0.
This release extends the REPL command system with typed parameters, a
built-in /help command, command aliases (/quit), and cancellation
via the 'Esc' key.
The default HTTP timeout is increased to 15 minutes (900s) to better
accommodate reasoning models and large structured outputs.
LLM::Agent#deserialize and LLM::Agent#restore now return self for
method chaining, and LLM::Buffer#pop is added for tail-end message
removal.
Tool resolution gains a fallback to the global LLM::Function registry
before raising LLM::NoSuchToolError, and pending_functions aliases
are added on both contexts and agents for a consistent interface.
Several REPL bugs are fixed including parameter state leakage across turns and invalid tool-call error routing.
Model metadata is refreshed across all providers with new Anthropic, OpenAI, Google, DeepInfra, DeepSeek, and xAI model entries.
Add
Buffer & function internals
buffer: add
LLM::Buffer#pop
AddLLM::Buffer#popfor removing the last message from the tail of the buffer, complementing the existing#<<and array-style message management.function: add registry fallback for tool resolution
When resolving tool calls from a message, if the tool is not found in the available tools list, it now also looks up the globalLLM::Functionregistry viaLLM::Function.find_by_namebefore creating a placeholder function. This improves tool resolution for tools that are registered globally but not passed directly through the request tool set.
Consistent pending_functions aliases
context: alias
LLM::Context#functionsasLLM::Context#pending_functions
AddLLM::Context#pending_functionsas an alias forLLM::Context#functions, so callers that prefer the more descriptivepending_functionsname can use it instead offunctionswhen checking for unresolved tool work.agent: alias
LLM::Agent#functionsasLLM::Agent#pending_functions
AddLLM::Agent#pending_functionsas an alias forLLM::Agent#functions, matching the same alias onLLM::Context, so callers have a consistentpending_functionsinterface across both contexts and agents.
REPL command system
- repl: extend command system with parameter support
Commands can now declare typed parameters using theparameterDSL, modelled afterLLM::ToolandLLM::Schemaconventions. Parameters can be marked as required withrequired %i[...], and values are type-checked before being passed tocall. Argument parsing is handled by the repl: arguments are split from the input string and assigned to parameters by position.
class Greeter < LLM::Command
name "greet"
description "Greets the given name"
parameter :name, String, "The person's name"
required %i[name]
def call(name:)
write("Welcome #{name}!\n")
end
end
- repl: add
helpcommand
AddLLM::Repl::Helpas a new built-in command, registered automatically via the command registry. Typing/helpshows thehelpcommand's own name, description, and parameters, while/help <name>shows details for a specific command, including its parameters and whether each is required or optional. Unknown command names produce an error message.
class Help < Command
name "help"
description "show help for a given command"
parameter :name, String, "The name of a command"
def call(name: nil)
if name.nil?
write("\n#{self.class.help}\n\n")
elsif command = LLM::Command.find_by(name:)
write("\n#{command.help}\n\n")
else
write "\nNo help for #{name} was found" \
"\nThat command doesn't exist.\n\n"
end
end
end
- repl: add support for command aliases
Commands can now be aliased by creating a subclass of another command (withLLM::Commandas an indirect ancestor). The first alias introduced is/quitas an alias of/exit.
class Quit < Command::Exit
name "quit"
end
repl: add
Command::Parameter#optional?
Parameters now expose an#optional?method that returnstruewhen a parameter has not been marked as required, making it possible to query parameter optionality programmatically.repl: add
LLM::Repl::Command#write
Commands can now write output to the transcript via thewritemethod. Commands also receive a reference to the active repl through their#initializemethod, making it possible to interact with the repl window from within a command.repl: display command errors in the curses UI
Commands invoked with too few arguments now display an error message:command(<name>): too few arguments. Displayed directly in the curses transcript area, giving immediate feedback instead of silently failing.repl: add
LLM::Commandconvenience constant
AddLLM::Command = LLM::Repl::Commandas a shorter alias, available once"llm/repl"is required.
Misc
- repl: implement cancellation with the 'Esc' key
The curses-based REPL now supports cancelling an active model request by pressing the 'Esc' key. When a request is in progress, the status line showsthinking • Esc to cancel, and pressing Esc callsLLM::Agent#cancel!to interrupt the request. The transcript displaysrequest cancelled!to confirm the cancellation.
Change
Misc
provider: increase default timeout to 900s
The default HTTP timeout for all providers has been increased from 180 to 900 seconds (15 minutes) to better accommodate long-running requests such as reasoning models and large structured outputs.agent:
deserializeandrestorereturnself
LLM::Agent#deserializeandLLM::Agent#restorenow returnself(the agent instance) instead of forwarding the context's return value, enabling method chaining after restoring agent state.context: discard all messages from a cancelled turn
WhenLLM::Context#cancel!is called, all messages added during that turn are now discarded viaBuffer#slice!, preventing edge cases where dangling tool calls between turns caused repeated cancellation loops. The#repair!method now handles tool call cancellations on the next turn instead of mutating the conversation buffer directly at cancellation time.stream: drop the
errorargument fromon_tool_call
Theon_tool_callcallback no longer accepts anerrorargument. Previously, stream parsers passed both a tool and an optional error, requiring boilerplate likeif error; queue << error; endin every callback. Error handling is now pushed directly onto the stream queue inside each provider's stream parser, soon_tool_call(tool)is the only signature. The REPL stream and baseLLM::Streamclass have been updated accordingly.
REPL internals
repl: pass the repl instance to command constructors
LLM::Repl::Commandsubclasses now receive the active repl instance viainitialize(repl), enabling commands to write to the transcript and interact with the repl window.repl:
Command#writeprefixes messages with the command name
The#writemethod now prefixes output withcommand(<name>):so command messages are consistent with theuser:andagent:labels in the transcript. The prefix can be customised with thewho:keyword argument, or set towho: nilto disable it entirely.
Fix
Misc
- function: avoid silent skip of tools not found in available tools
When a model calls a tool that is not present in the available tools list, instead of silently skipping the tool call (vianext), aLLM::NoSuchToolErroris now raised so the model receives feedback about the invalid tool call and can correct course.
An additional fallback to the globalLLM::Functionregistry is tried before raising, so globally registered tools are still resolved even when not in the per-request tool set.
REPL bugs
repl: don't persist parameter state between turns
Parameter state (such asParameter#value) was leaking across turns because the same parameter objects were being mutated in place. A duplicate set of parameters is now created for each turn, keeping the original parameter definitions intact and preventing stale state from carrying over.repl: reply with error when given an invalid tool
When the model tries to call a tool that does not exist, the error is now pushed onto the stream queue so the model can see the error and correct course, instead of silently dropping the invalid tool call and leaving it toContext#repairto remove it from history.repl: fix save of initial runtime state
Fix a bug inLLM::Repl#configurewhere a non-existent path argument was treated as no path at all, preventing the initial runtime state from being saved after the first turn. The correct behavior is to create the file so it can be written to after the first turn completes.
Refresh
- Refresh model metadata across all providers
Update model listings, pricing, capabilities, reasoning options, modality support, context limits, and release dates across all provider registries (Anthropic, AWS Bedrock, DeepInfra, DeepSeek, Google, Mistral, OpenAI, xAI, and ZAI). Notable changes include Anthropic claude-opus-4-8 and claude-sonnet-4-6 additions with effort-based reasoning, OpenAI gpt-5.6-sol/terra/luna and gpt-5-codex additions, Google gemini-3-pro-preview and gemini-3-flash-preview additions, DeepInfra Qwen3.5 and DeepSeek V4 model additions, and updated xAI Grok model entries.
v12.4.0
Changes since v12.3.1.
This release brings major improvements to the curses-based REPL
(LLM::Agent#repl). The REPL now supports saving and restoring runtime
state across sessions, automatic paste-mode detection for fast bulk input,
a command system foundation with the /exit command, and several new
keybindings (Ctrl+F, Ctrl+K, Ctrl+Y). Tool calls are rendered with a
compact function-call syntax in the status bar.
Two new built-in tools: LLM::Tool::Ls and LLM::Tool::Which are
available as opt-in additions for file listing and executable lookup.
Model metadata has been refreshed across providers, the REPL loop
internals have been refactored to use catch/throw for cleaner command
routing, and several bugs have been fixed including a tracer restoration
issue in the agent ensure clause and a missing cursor in the REPL input
area.
Add
repl: allow runtime state to be saved and restored
LLM::Agent#replnow accepts apath:option that serializes runtime state to the filesystem. When the path already exists, runtime state is restored when the read-eval-print loop starts. Otherwise the path is written after the first turn, making it possible to resume a session across process restarts.repl: scroll to the bottom on submit
The curses-based REPL now scrolls the transcript to the bottom when the user submits their input, so the latest response is visible without needing to scroll down manually.repl: add Ctrl+F to move the cursor forward
The curses-based REPL input now supports Ctrl+F to move the cursor forward by one column, matching common terminal editing conventions found in shells like/bin/sh.repl: add Ctrl+K to erase from cursor to end of line
The curses-based REPL input now supports Ctrl+K to erase all text from the cursor position to the end of the input buffer, matching common terminal editing conventions found in shells like/bin/sh.repl: add Ctrl+Y to paste previously killed text
The curses-based REPL input now supports Ctrl+Y to insert the most recently killed text (via Ctrl+K) at the current cursor position, matching the yank/paste convention found in shells like/bin/sh. The killed text is stored in an internal copy buffer so it can be pasted multiple times or at different cursor positions.repl: add command system foundation
AddLLM::Repl::Commandas a new base class for REPL commands, along with the first built-in commandLLM::Repl::Command::Exitwhich exits the read-eval-print loop viathrow(:exit). Commands are identified by a name and can be looked up throughCommand.find_by. This is the foundation for the/command syntax used in the REPL input line.repl: connect the command system to user input
The curses-based REPL now routes user input through the command system. Any input string beginning with"/"is matched against the command registry viaCommand.find_by, and the corresponding command is executed instead of being forwarded to the model. This makes built-in commands like/exitfunctional from the input line. Command arguments are not yet supported.repl: add
LLM::Repl::Command.registry
AddLLM::Repl::Command.registryfor auto-registering command subclasses. Theinheritedhook captures each new subclass and stores it in the registry, making it possible to enumerate all available commands at runtime. Built-in commands like Exit are automatically registered when the command file is loaded.repl: detect and handle paste mode in the input line
The curses-based REPL input now detects paste operations by tracking the rate at which characters arrive. A paste rate of ≤50ms is assumed to be a burst of characters that could only be explained by a paste. No human types that fast. Multiline pastes are supported through internal refactoring of the input handling logic.repl: optimize paste mode rendering
Track the paste state with an internal@pastevariable and switch to a faster input path during paste operations. While in paste mode, the input buffer is drained viaCurses.getch, bypassing the more expensive char-by-char render path used for ordinary interactive input. This makes pasting large amounts of text noticeably faster.Add
LLM::Tool::Ls
Add a built-in tool for listing files and directories, with optional glob pattern filtering to narrow results.
It must be required explicitly withrequire "llm/tools/ls".Add
LLM::Tool::Which
Add a built-in tool for locating an executable on the system PATH. This lets an agent check whether a command is available before attempting to run it, avoiding failed subprocess calls.
It must be required explicitly withrequire "llm/tools/which".repl: render tool calls in a function-call syntax
The curses-based REPL status bar now renders tool calls with a compact function-call syntax:tool(key: value)instead oftool: name. Strings are quoted and truncated, arrays show their first two elements, and hashes collapse to{…}, making it easier to see what arguments the model is passing. Thetool donestatus message has been removed since the tool call itself conveys completion information.
Change
Refresh model metadata
Update model listings, pricing, and capabilities across providers. Fix GPT-5.6 model family names in the OpenAI registry (gpttogpt-sol,gpt-nanotogpt-luna,gpt-minitogpt-terra). Add OpenAI models (gpt-5.6-luna,gpt-5.6-sol,gpt-5.6-terra) to the AWS Bedrock registry. Update DeepInfra pricing forDeepSeek-V3andSky-T1-32B-Preview. Fix Google model knowledge cutoff dates.repl: control the loop with catch & throw
The curses-based REPL input loop now usescatch(:exit)andthrow(:exit)instead of returning the:exitsymbol and breaking out of the loop. This enables the/commandsyntax without requiring an:exitreturn value to be propagated through a potentially deeply nested call path.repl: replace Ctrl+D with shell-like delete-at-cursor
The curses-based REPL input now treats Ctrl+D as a delete action that removes the character at the current cursor position, matching the shell/Emacs convention where Ctrl+D deletes the character under the cursor instead of signalling end-of-file. The previous Ctrl+D behaviour (exiting the REPL) is superseded by the/exitcommand.repl: switch to 'Thinking' mode after tool return
The curses-based REPL status line now switches to "Thinking" mode after a tool returns, so the user can see the agent is processing the tool result rather than showing a stale tool-call status.
Fix
agent: fix a subtle typo in the ensure clause
Fix a subtle typo inLLM::Agentwhere the deprecatedtracelocal variable was given preference overtracer(the preferred local name) in anensureclause. Thetracelocal was supported for backward compatibility but the ensure clause still referencedtraceinstead oftracer, which meant the previous tracer was never restored when the REPL session ended.repl: restore the cursor in the input area
Remove theCurses.curs_set(0)call from the REPL redraw method, which was inadvertently hiding the cursor and making it impossible to see the current position in the input area. The input field is now always drawn at its full height so the cursor position is correctly maintained after each redraw.
v12.3.1
Changes since v12.3.0.
This release fixes a flickering issue in the curses-based REPL redraw. The full-screen clear that caused visible flickering has been replaced with a targeted cursor-hide approach, and stale rows from a larger transcript are now explicitly cleared to prevent ghost text from lingering when the transcript shrinks.
Fix
- repl: fix redraw flicker
ReplaceCurses.clearwithCurses.curs_set(0)in the REPL redraw method to avoid a full screen clear that caused visible flickering during redraws. The drawing order is also adjusted so the status line is drawn before the divider, and stale rows left over from a larger transcript are now explicitly cleared to prevent ghost text from lingering when the transcript shrinks.
v12.3.0
Changes since v12.2.0.
This release brings major improvements to the curses-based REPL
(LLM::Agent#repl). The status line now shows a context-usage bar and
running cost counter, the input field expands to three rows with
full cursor navigation, model responses are rendered as styled markdown,
and the UI stays responsive while the agent is working by running
requests in a separate thread. A new LLM::Stream::IO and
LLM::Stream::Disabled provide a uniform stream representation across
all stream types.
Mistral OCR support is added for extracting text from images and
documents via the /v1/ocr endpoint. The skills: and tools: options
on LLM::Agent#repl let you attach additional tools or skill directories
for the duration of a session. LLM::Object#merge! rounds out the
in-place merge API, and a new LLM.logger convenience method creates
tracer logger instances with less verbosity.
Add
Add
LLM.loggerconvenience method
AddLLM.logger(llm, ...)as a shorter, less verbose way to create anLLM::Tracer::Loggerinstance. Takes a provider and optional keyword arguments forwarded to the logger constructor.Add
skills:option toLLM::Agent#repl
LLM::Agent#replnow accepts askills:keyword argument that attaches one or more skill directories (containingSKILL.md) for the duration of the repl session. Skills are loaded and converted to tools, combining with any tools already configured on the agent, and are discarded when the session ends.Add
LLM::Provider#ocrbase method
Add a baseocr(...)method toLLM::Providerthat raisesNotImplementedErrorby default, establishing a common interface for providers that support OCR (Optical Character Recognition) on images and documents.Add Mistral OCR endpoint support
The Mistral provider now supports OCR via its/v1/ocrendpoint. Callmistral.ocr(image_url: ...)for images ormistral.ocr(document_url: ...)for documents (e.g., PDFs). Returns anLLM::Responsewith extracted pages, markdown content, and structured block data.Add
LLM::Object#merge!
AddLLM::Object#merge!for in-place merging of hash data into anLLM::Objectinstance, complementing the existing#mergemethod.Add
LLM::Stream::IOandLLM::Stream::Disabled
LLM::Stream::IOwraps IO-like objects as stream targets, forwarding streamed content via#<<.LLM::Stream::Disabledrepresents an explicitly disabled stream with no-op callbacks.
This is part of an internal refactoring that lets all stream values: IO
objects, true, false, nil, and LLM::Stream instances themselves
be represented by the same LLM::Stream interface via the new
LLM::Stream.try factory method.
Before this change the codebase had to perform ad-hoc type checks
(e.g. if LLM::Stream === stream) scattered throughout. After this
change all stream handling goes through a single uniform path, and
providers check #enabled? to decide whether to request streaming
from the API.
Change
repl: rename
trace:totracer:
Thetrace:keyword argument inLLM::Agent#replhas been renamed totracer:for consistency with the rest of the codebase. The oldtrace:name still works with a deprecation warning.repl: add context-usage bar and cost counter to the status line
The curses-based REPL status line now shows a small progress bar that indicates how much of the model's context window remains as a percentage, alongside a running cost estimate rendered on the right side of the status line. The input line has been updated to show the provider name as a prefix. Estimates are best-effort and depend on registry pricing data (seedata/).repl: keep the UI responsive while a request is in progress
The curses-based REPL now spawns the agent request in a separate thread and communicates streamed output through a queue, so the curses UI stays responsive during model processing. Users can continue to scroll through the transcript while the agent is working.repl: style transcript rows as structured data with bold labels
The curses-based REPL transcript now stores rows as structured data with style metadata instead of plain strings, enabling bold rendering of theuser:andagent:labels for improved readability during interactive sessions.repl: render a small subset of markdown
The curses-based REPL now renders model responses as styled markdown. Headers and strong text render in bold, emphasis renders in underline, and code spans and blocks are highlighted with inverted colors. Streaming content is buffered and re-rendered on each tick so the transcript reads cleanly as the agent responds. Requires the optionalkramdowngem.repl: add cursor LEFT/RIGHT movement to the input line
The curses-based REPL input now supports cursor movement with the left and right arrow keys, enabling in-place text editing before submitting a prompt. The cursor position is tracked visually and moves backwards on left-arrow and forwards on right-arrow.repl: add Ctrl+A and Ctrl+E keybindings to the input line
The curses-based REPL input now supports Ctrl+A to jump the cursor to the start of the input line and Ctrl+E to jump it to the end, matching common terminal editing conventions.repl: add
tools:option toLLM::Agent#repl
LLM::Agent#replnow accepts atools:keyword argument that attaches additional tool classes or instances for the duration of the repl session. These tools are combined with any tools already configured on the agent, and are discarded when the session ends.repl: add repl support to ActiveRecord and Sequel agent models
acts_as_agent(ActiveRecord) andplugin :agent(Sequel) models now expose areplmethod that delegates to the underlying agent's read-eval-print loop. This allows interactive debugging and inspection of persisted agent state at runtime. Note that changes made during a repl session do not persist back to the database.repl: add extra padding between markdown nodes
The curses-based REPL markdown renderer now adds extra vertical spacing between certain markdown elements: paragraphs, headers, and codeblocks for improved readability of model responses.repl: add a visual divider between transcript and the rows below it
The curses-based REPL now draws a horizontal divider line (using a unicode─character) to separate the transcript area from the status and input rows below it. A single empty buffer row is also added between the transcript and the divider, preventing transcript text from running too close to the status and input rows.repl: expand input field to 3 rows
The curses-based REPL input field now spans three rows instead of one, wrapping text that exceeds the terminal width onto subsequent lines. A scrollable viewport follows the cursor so the active line stays visible, and common navigation commands (Ctrl+A, Ctrl+E, cursor keys) work across all three rows of the expanded input area.Refresh OpenAI model metadata
Add new OpenAI models to the registry, includinggpt-5.6,gpt-5.6-luna,gpt-5.6-terra,gpt-5.6-sol, andgpt-realtime-2.1, with associated pricing, capabilities, and limits.
Fix
Fix Ollama non-streaming response handling
Fix the Ollama provider to properly handle the non-streaming path. When the provider returns a raw NDJSON response body (instead of streaming), the response is now parsed and merged into a singleLLM::Objectbefore being returned to the caller. Previously the non-streaming path was effectively broken and would fail to produce a valid completion response.repl: handle a negative context window allowance in the usage bar
Fix a crash in the curses-based REPL context-usage bar when the context window allowance is exceeded (used > total). The negative width value that resulted from this edge case could cause curses errors; it now gracefully defaults to0%and zero bar width.Fix YARD documentation across provider and tool files
Fix unnamed, misnamed, and missing@paramtags inLLM::Repl::Status,LLM::Tool::Git,LLM::Tool::Pwd,LLM::Tool::Rg, andLLM::Tool::SwapText.
v12.2.0
Changes since v12.1.0.
This release adds Mistral as a new provider with chat completions, streaming, tool calls,
structured outputs, file/image attachments, and embeddings support. It introduces
the trace: option to LLM::Agent#repl for keeping the tracer active during
interactive sessions.
Several fixes land for the Google provider (generationConfig parameter leakage),
LLM::Context#tracer= (always assigning nil), LLM::Provider#with_tracer(nil)
(nil fallback), and LLM::Context#repair! (dropping Struct returns).
The default HTTP timeout has been increased from 60s to 180s to better accommodate
reasoning models and large structured outputs, and the Anthropic default model has
been updated to claude-opus-4-8. Model metadata has been refreshed across
Anthropic, AWS Bedrock, DeepInfra, Google, and xAI, with Mistral model data added
to the registry.
Add
Add
trace:option toLLM::Agent#repl
LLM::Agent#replnow accepts atrace:keyword argument. By default the tracer is disabled for the duration of the repl session to prevent curses UI interference from output written to$stdoutor$stderr. Settrace: trueto keep the tracer active during the session, which is useful when the tracer writes to a file rather than the terminal.Add a new provider: LLM::Mistral
Mistral is now supported through its OpenAI-compatible API. The provider supports chat completions, streaming, tool calls, structured output (schema), file/image attachments, and embeddings. UseLLM.mistral(...)to create a provider instance.Add
LLM.mistral(...)convenience method
A new top-level accessor (LLM.mistral) returns anLLM::Mistralprovider instance, matching the pattern used by other providers.
Fix
Fix Google
generationConfigparameter leakage
Fix a bug in the Google provider where non-generation parameters (role,model,messages,stream) were leaking into thegenerationConfigobject alongside legitimate generation config parameters such astemperature. Non-config parameters are now filtered out before constructinggenerationConfig.Fix
LLM::Context#tracer=always assigningnil
TheLLM::Context#tracer=setter had a bug where it always assignednilregardless of the tracer value passed. It now correctly assigns the given tracer or falls back toLLM::Tracer::Null.Fix
LLM::Provider#with_tracer(nil)fallback
LLM::Provider#with_tracer(nil)now falls back toLLM::Tracer::Nullinstead of setting aniltracer directly.Fix
LLM::Context#repair!droppingStructreturns
LLM::Context#repair!used[*prompt]to wrap the prompt before grepping for return objects. SinceLLM::Function::Returnis aStruct, the splat operator expanded it into its member values instead of wrapping it, causing the grep to silently drop returns. The fix wraps both sources in an array before flattening.
Change
Increase default provider timeout from 60s to 180s
The default HTTP timeout for all providers has been increased from 60 to 180 seconds to better accommodate long-running requests such as reasoning models and large structured outputs.Change Anthropic default model to
claude-opus-4-8
The default Anthropic chat model has been updated fromclaude-sonnet-4-20250514toclaude-opus-4-8, reflecting the latest model release from Anthropic.Refresh model metadata
Update model listings, pricing, and capabilities for Anthropic, AWS Bedrock, DeepInfra, Google, and xAI. Add Mistral model data to the registry.
v12.1.0
Changes since v12.0.0.
This release adds LLM::Agent#repl with a curses-based
interactive read-eval-print loop. It requires the optional
dependency curses and it is probably the most notable
feature in this release.
Multiple opt-in tools have been added to the llm/tools/*.rb
directory. They serve as examples and as general-purpose tools
that happen to power the repository's agents.
Other changes include small-ish bug fixes.
As always, see the changelog details for a thorough overview.
Add
Add
LLM::Agent#repl
Add a curses-based read-eval-print loop forLLM::Agentthat lets developers interact with an agent after it has been set up or has performed a task. It is similar tobinding.pry: once you exit, you can continue with the rest of your program. It requires thecursesgem.Add
#tracer=setter on Provider, Context and Agent
LLM::Provider,LLM::ContextandLLM::Agentcan now configure the tracer after initialization via the#tracer=setter. It accepts a subclass ofLLM::Tracerornilto disable the tracer.Add
LLM::Tool::Shell
Add a built-in shell tool that can run a command with arguments.
It must be required explicitly withrequire "llm/tools/shell"and requires thetest-cmd.rbgem.Add
LLM::Tool::ReadFile
Add a built-in tool for reading the contents of a file, with optionalstartandstopline offsets.
It must be required explicitly withrequire "llm/tools/read_file".Add
LLM::Tool::Chdir
Add a built-in tool for changing the current working directory.
It must be required explicitly withrequire "llm/tools/chdir".Add
LLM::Tool::Git
Add a built-in tool that can perform git actions (log,diff,show,commit,checkout,branch).
It must be required explicitly withrequire "llm/tools/git"and requires thetest-cmd.rbgem.Add
LLM::Tool::Rg
Add a built-in tool that wraps therg(ripgrep) command for recursively searching the current directory for patterns.
It must be required explicitly withrequire "llm/tools/rg"and requires thetest-cmd.rbgem.Add
LLM::Tool::SwapText
Add a built-in tool that can replace an exact snippet of text in a file with a new piece of text.
It must be required explicitly withrequire "llm/tools/swap_text".Add
LLM::Tool::Pwd
Add a built-in tool that returns the current working directory.
It must be required explicitly withrequire "llm/tools/pwd".Add
LLM::Tool::WriteFile
Add a built-in tool that can write a given string to a given file path.
It must be required explicitly withrequire "llm/tools/write_file".Add
LLM::Tool::Mkdir
Add a built-in tool that can create a tree of new directories.
It must be required explicitly withrequire "llm/tools/mkdir"and requires thetest-cmd.rbgem.
Change
Change LlamaCpp default port (8080 => 8013)
The default port for the LlamaCpp provider has changed from8080to8013since llamacpp itself defaults to that port.Change LlamaCpp default model to
nil
The default model for LlamaCpp is nownil, letting whatever model is served by the llamacpp server act as the default. Previously it defaulted toqwen3.
Fix
Fix
LLM::Agent.toolsSymbol resolution
When an agent defined tools viatools :method_name, the resolved symbol was incorrectly forwarded as[:method_name](an array) toLLM::Context. This fix copies the same pattern used by other attribute resolvers (e.g.,skills) so a single Symbol is resolved through the agent instance correctly.Encode strings as UTF-8 in the JSON adapter
Thejsongem will reject BINARY-encoded strings from version 3 and beyond. TheLLM::JSONAdapter.dumpmethod now walks serialized data and encodes every string into UTF-8, usingString#scrubto replace bytes that are not valid UTF-8.