Try llm4s in the REPL
Ask a model a question in one line, from the Scala CLI REPL, a Metals worksheet or a script.
Table of contents
llm4s-repl puts a handful of helpers behind one import: ask streams an answer to the console, extract reads a
typed value out of text, chat() is a small read-eval loop, and history and lastUsage show the conversation and
what the last call cost. A failed call prints the error and its troubleshooting hint, never a stack trace.
It is for trying things out, not for production code: the helpers print to the console and keep the conversation
in memory, and no other llm4s module depends on llm4s-repl, so it never reaches an application unless you add it.
It is experimental. For an application, use LLMClient and the agent directly.
The dependency
llm4s-repl is new: it is published from the first release after 0.5.1. It depends on
llm4s-core, llm4s-schema-derivation (for extract) and llm4s-testkit (for the offline session). Add the
provider module you want to call, as for any llm4s application (Installation).
In a Scala CLI script or worksheet:
1
2
//> using dep org.llm4s::llm4s-repl:<version>
//> using dep org.llm4s::llm4s-openai:<version>
In sbt, for the console task:
1
libraryDependencies += "org.llm4s" %% "llm4s-repl" % llm4sVersion
A session
Every helper takes a Session from a context parameter, so you set it once:
1
2
3
4
5
6
7
8
9
import org.llm4s.repl.*
import org.llm4s.testkit.Reply
// The section `llm4s.providers.provider` names in application.conf
val fromConfig = Session.fromConfig()
// A named section, llm4s.providers.openai-main
val named = Session.named("openai-main")
// No key and no network: the first call gets the first reply, and so on
given Session = Session.offline(Reply.text("Covariance lets a List[Cat] be used where a List[Animal] is expected."))
Session.of(client) wraps a client you built yourself. A session whose configuration does not load is still a
session: session.failure holds the error, and every helper prints it. withSystemPrompt, withOptions (any
CompletionOptions) and withMaxToolSteps return a new session with an empty history.
Ask, extract, chat
1
2
3
4
5
6
7
8
9
10
11
12
import org.llm4s.repl.*
import org.llm4s.testkit.Reply
given Session = Session.offline(
Reply.text("Covariance lets a List[Cat] be used where a List[Animal] is expected.").withUsage(
org.llm4s.llmconnect.model.TokenUsage(12, 14, 26)
)
)
val answer = ask("Explain variance in Scala 3 in one sentence") // streams the answer, then returns it
history.show // the conversation so far
lastUsage // Some(scripted-model: 12 prompt + 14 completion = 26 tokens, 3 ms, cost unknown)
ask returns an Outcome[String]: Outcome.Done(text), which the REPL prints as the text, or
Outcome.Failed(error), printed as error: <kind>: <message> with the hint llm4s attaches to configuration,
authentication, rate-limit and not-found errors. answer.getOrElse(""), answer.toOption and answer.toResult give
the value. Each question and answer is added to the history, so the next ask carries on the conversation.
extract asks for a typed value, with the JSON schema derived from the type (see
Derived Schemas):
1
2
3
4
5
6
7
8
9
10
import org.llm4s.repl.*
import org.llm4s.schema.SchemaOf
import org.llm4s.testkit.Reply
import upickle.default.ReadWriter
case class Invoice(vendor: String, amount: Double, currency: String) derives SchemaOf, ReadWriter
given Session = Session.offline(Reply.text("""{"vendor":"ACME","amount":12.5,"currency":"EUR"}"""))
val invoice = extract[Invoice]("ACME, 12.50 EUR, two widgets") // Done(Invoice(ACME,12.5,EUR))
chat() reads lines from the console and asks each one; :history prints the conversation, :reset forgets it and
:q leaves.
1
2
3
4
5
scala> chat()
Chatting with default provider, gpt-4o-mini. :q leaves, :history shows the conversation, :reset forgets it.
you> What is a monad?
llm> A monad is ...
you> :q
Tools
Give ask a ToolRegistry and it runs the tools the model calls, printing each call and its result:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.llm4s.repl.*
import org.llm4s.testkit.Reply
import org.llm4s.toolapi.{ Schema, ToolBuilder, ToolRegistry }
val weather = ToolBuilder[Map[String, Any], ujson.Value](
"get_weather",
"The weather in a city",
Schema.`object`[Map[String, Any]]("Arguments").withProperty(Schema.property("city", Schema.string("The city")))
).withHandler(args => args.getString("city").map(city => ujson.Obj("city" -> city, "temperature" -> 21)))
.buildSafe()
given Session = Session.offline(
Reply.toolCall("get_weather", """{"city":"Paris"}"""),
Reply.text("It is 21 degrees in Paris.")
)
weather.foreach(tool => ask("Weather in Paris?", tools = new ToolRegistry(Seq(tool))))
// -> get_weather({"city":"Paris"})
// <- {"city":"Paris","temperature":21}
// It is 21 degrees in Paris.
Scripts: strict mode
A script usually wants to stop at the first failure. The helpers of strict return a Result and print no error:
1
2
3
4
5
6
7
8
9
import org.llm4s.repl.*
import org.llm4s.testkit.Reply
given Session = Session.offline(Reply.text("Paris."), Reply.text("About two million."))
val answers = for {
capital <- strict.ask("Capital of France?")
population <- strict.ask(s"Population of $capital?")
} yield population
Where to run it
- The Scala CLI REPL:
scala-cli repl --dep org.llm4s::llm4s-repl:<version> --dep org.llm4s::llm4s-openai:<version>, thenimport org.llm4s.repl.*andgiven Session = Session.fromConfig(). Scala CLI readsapplication.conffrom a resource directory (--resource-dir resources). - A Metals worksheet: a
*.worksheet.scfile with the//> using deplines above. Metals shows each value beside its line;askalso prints the answer. - A Scala CLI script: a
*.scfile, run withscala-cli run hello.sc. - Almond notebooks are tracked in #2296; this page will link them once they exist.
The repository’s examples/repl has a script and a
worksheet that run against Session.offline; CI runs both with Scala CLI against the library built from the
branch, with no API key.