API Stability
This page says how binary compatibility is checked and which modules are covered; the promise it serves, and how an API is deprecated and removed, are in the Compatibility and Deprecation Policy. Which packages are stable, beta or experimental is defined in 1.0 Scope, the source of truth for tiers; this page does not repeat that list, so the two cannot drift.
Binary compatibility is enforced between releases with MiMa.
Stability Contract
| Release type | Guarantee |
|---|---|
| Patch (0.x.y to 0.x.z) | No binary-breaking changes in frozen modules |
| Minor (0.x to 0.y) | Binary-breaking changes allowed, with a @deprecated migration path where one exists |
| 1.0.0 and later | Full SemVer: a MAJOR version for breaking changes |
Tiers in the Code
1.0 Scope says which packages are frozen, but a table drifts from the code it describes. So
every top-level public type of a frozen module also carries its tier, from org.llm4s.annotation in
llm4s-core:
| Annotation | Meaning |
|---|---|
@Stable |
Covered by the 1.x compatibility promise: removed only after a deprecation cycle, and checked by MiMa. |
@Experimental |
Not covered. It can change or disappear in a minor release, with a migration note in that release’s CHANGELOG. |
Both are Java annotations with runtime retention, so an IDE, a tool or a Java caller can read them; a
Scala-only annotation would be invisible to all three. A companion object needs no annotation of its
own: the one on its class, trait or enum covers the pair. A type that is private or private[x] needs
none. A type in a module 1.0 does not freeze (anything Beta or Experimental in the
Package Map) carries neither.
sbt stabilityTierCheck (a CI quick check) fails the build for a top-level public type of a frozen
module that has neither annotation or both, and for a Beta dialect that lives inside a frozen module
and is not @Experimental: today the Mistral and Cohere dialects of llm4s-openai-compatible, listed in
build.sbt. It reads sources, not classes, because Scala compiles private[llm4s] to a public
bytecode member. It covers llm4s-core, llm4s-openai, llm4s-openai-compatible,
llm4s-anthropic, llm4s-gemini and llm4s-ollama. llm4s-agent is frozen by 1.0 Scope but is not
covered yet: the typed graph runtime (#1266) is
replacing its execution model and its tier is an open question in
#1281. Add it to stabilityTierModules in build.sbt
when that is settled.
A type annotated @Experimental inside a frozen module needs a ProblemFilters.exclude entry that says
why when the baseline is set (see The Baseline); the annotation is what tells you which.
What MiMa Covers
MiMa runs only on the modules 1.0 Scope freezes. Each calls mimaFrozen("<artifact>") in
build.sbt:
| Module | Artifact |
|---|---|
modules/core |
llm4s-core |
modules/agent |
llm4s-agent |
modules/openai |
llm4s-openai |
modules/openai-compatible |
llm4s-openai-compatible |
modules/anthropic |
llm4s-anthropic |
modules/gemini |
llm4s-gemini |
modules/ollama |
llm4s-ollama |
Every other module is Beta or Experimental, or is not published (llm4s-samples,
llm4s-workspace-*, llm4s-it, llm4s-docs, llm4s-benchmarks), and is not checked. That includes
org.llm4s.speech.* (llm4s-speech), org.llm4s.runner.* (llm4s-workspace-runner) and
org.llm4s.samples.* (llm4s-samples, llm4s-workspace-samples): none ships in a frozen module, so
no filter is needed for them. Anything a frozen module contains that 1.0 Scope marks Beta or Experimental needs a
ProblemFilters.exclude entry that says why; there are none yet because no baseline is set (see
The Baseline).
If you find yourself importing from a Beta or Experimental package, please open an issue: it likely means the stable API is missing something.
What a Frozen Module May Depend On
Freezing a module freezes what it puts on its users’ classpath, so a frozen module may not resolve a
document-parsing, speech, cloud-storage, database or observability-backend dependency, or another provider’s
vendor SDK, directly or transitively. sbt frozenDependencyCheck (a CI quick check) reads each frozen
module’s resolved runtime dependencies and fails the build for a forbidden group, saying whether the module
declares it or reaches it transitively. The list, and where each group belongs instead, is in
project/FrozenDependencies.scala; change it there, with the reason, if a frozen module genuinely needs one.
llm4s-openai and llm4s-anthropic each keep their own vendor SDK; no other frozen module may carry either.
The Baseline
mimaBaselineVersion in build.sbt names the release each frozen module is compared with. It is
None for now, so sbt mimaReportBinaryIssues checks nothing and CI passes.
The last release, 0.4.1, is a single llm4s-core of the pre-modularisation code, so it is not a usable
baseline for the split modules: against llm4s-core 0.4.1 MiMa reports thousands of problems, and
llm4s-agent and the provider modules do not exist at 0.4.1, so their artifacts do not resolve. The
baseline is 0.5.0, the first release with the split coordinates
(#1281).
Setting and bumping the baseline
mimaBaselineVersion is a plain val, not an sbt setting, so it is edited in the file; it cannot be
changed with set:
- Publish the release and confirm every frozen artifact is on Maven Central under its
_3name, for examplehttps://repo1.maven.org/maven2/org/llm4s/llm4s-agent_3/0.5.0/. A frozen module whose baseline is not published failsmimaPreviousClassfileswith a “not found” error, by design. - Change the line to
val mimaBaselineVersion: Option[String] = Some("0.5.0"). - Run
sbt mimaReportBinaryIssues. Fix real breaks. Add a filter (below) only for an intentional one. - When the baseline later moves to a newer release, delete every
mimaBinaryIssueFiltersentry first: each one excuses a break against the old baseline, so against the new one it is stale and would silently hide a real break in the same class. Then re-run the check and re-add only what the new baseline needs. - Update this page and the CHANGELOG entry if the covered module list changes.
Before the first run against 0.5.0, review the Beta parts that ship inside frozen modules, because
1.0 Scope does not freeze them: org.llm4s.assistant.* (llm4s-agent) and the Mistral and
Cohere dialect classes (llm4s-openai-compatible). Either exclude them with a documented filter or
decide that they are frozen too. llm4s-agent may also wait for the graph runtime
(#1266) before it gets a baseline; if so, drop
mimaFrozen("llm4s-agent") from modules/agent until then.
The build is Scala 3 only, so one sbt mimaReportBinaryIssues covers every artifact. If a second Scala
version returns, run sbt +mimaReportBinaryIssues in CI.
Adding a Binary-Incompatible Change
Once a baseline is set, a change to a frozen module that breaks binary compatibility needs:
- A
@deprecatedversion of the old API pointing to the new one, where that is possible - A
ProblemFilters.excludeentry in the module’smimaBinaryIssueFilters, with a comment saying why the break is intentional - An entry in
CHANGELOG.mdunder[Unreleased]
ProblemFilters is not in sbt’s default imports, so build.sbt needs
import com.typesafe.tools.mima.core._ at the top before a filter compiles.
1
2
3
4
5
// build.sbt, in the module's settings
mimaBinaryIssueFilters ++= Seq(
// Agent.run gained a parameter; the old overload is deprecated, not removed.
ProblemFilters.exclude[DirectMissingMethodProblem]("org.llm4s.agent.Agent.run")
)
Use the narrowest problem type and the narrowest name that matches, not [Problem] with a wildcard.
Checking Compatibility Locally
1
sbt mimaReportBinaryIssues
This reports binary incompatibilities between the current code and the baseline release. With no
baseline set, each module logs mimaPreviousArtifacts not set (or is empty) and the task succeeds.
With a baseline set, success means the frozen API is compatible, and failure lists each problem with
the ProblemFilters.exclude line that would silence it. To check one module, run
sbt agent/mimaReportBinaryIssues.