← Back to all posts
September 24, 2026·Mellea Contributors

Audio in, files out, and a much faster CLI: Mellea v0.8.0

Mellea v0.8.0 was released on 23 September 2026. Most of it makes things you already do easier:

  • Audio input. Hand a model a recording the same way you already hand it an image.
  • Files out of the sandbox. What your generated code produced comes back on the result, instead of staying in the container.
  • Streaming you drive yourself. A plain async iterator, with no background task to remember.
  • Your own adapters. A LoRA or aLoRA you trained registers directly.
  • Granite 4.2 by default. A fresh install pulls a current local model.
  • A much faster m. Startup down from 3.2s to 0.1s.

There are some breaking changes too. If you use streaming, telemetry dashboards, tool calls, requirements alongside a sampling strategy, or you call the adapter verbs directly, give breaking changes a few minutes before you upgrade. For everything else this should be a straight version bump.

Audio in, text out

Audio now works the way images already did (#1396):

from mellea import start_session
from mellea.core import AudioBlock

with start_session(
    "openai",
    model_id="my-audio-model",
    base_url="http://localhost:8088/v1",
    api_key="default",  # local servers ignore it, but one is required
) as session:
    result = session.instruct(
        "Explain what is in this recording using bullet points",
        audio=[AudioBlock.from_file("meeting.wav")],
        strategy=None,
    )
    print(result.value)

AudioBlock.from_file(), from_url() and from_bytes() all detect the format from the data itself rather than trusting a file extension (#1601). For a clip you reuse across turns, pass an AudioUrlBlock instead and the download is deferred and cached per URL. A backend that cannot accept audio now tells you before the request goes out (#1410). Images got the same treatment, including a make_image_block() helper (#1377).

m serve handles audio too (#1443). There are two worked examples: a single call through llama-server, and a two-step Granite path on Ollama that transcribes first, then queries the transcription with a requirement checking the result. Both are in docs/examples/m_serve/multimodal-audio/, and the plain session examples are in docs/examples/audio_text_models/audio_examples.py.

Files out of the sandbox

v0.7.0 gave agents a sandboxed Python interpreter, which was useful right up to the moment the generated code produced a file. Plots, CSVs, trained models: all of it stayed in the container, and retrieving it was your problem.

A successful local-tier run now returns files from the tool's working directory on the result, as ExecutionResult.artifacts (#1384). Docker-backed tools can export container paths the same way, for the paths you list in CapabilityPolicy.artifact_export_paths, and only when the environment is used as a context manager rather than one-shot.

A much faster CLI

Top-level imports had leaked into the m entry point, so every invocation paid to load the whole library. With them removed, m --help goes from 3.170s to 0.113s, as measured in #1537. If you use the CLI interactively, this is probably the change you will feel most.

Streaming you drive yourself

The old API asked you to track two things: the stream you iterated, and a background task you eventually awaited. Forgetting acomplete() leaked that task, and so did breaking out of the loop early.

stream() is one object you consume directly, and async with cancels the generation on every exit path, including an early break (#1543, #1567):

from mellea.stdlib.streaming import stream

async with await stream(
    action, backend, ctx, requirements=[req], chunking="sentence"
) as streamer:
    async for chunk in streamer:
        print(chunk)

Typed events come from the same call with as_events=True, and the event vocabulary is unchanged. There is also a new streamer.completed_normally, which unlike not streamer.failed_early is False after an early break.

Validation at the granularity each check needs

A stream has one chunk boundary, but requirements do not all want the same one. "No sentence exceeds twelve words" needs whole sentences; a banned-word check wants single words. Previously both had to share the stream's setting.

Each Requirement can now declare its own (#1630):

from mellea.core.backend import Backend
from mellea.core.base import Context
from mellea.core.requirement import PartialValidationResult, Requirement

class MaxWordsPerSentence(Requirement):
    def __init__(self, limit: int = 12) -> None:
        super().__init__(description="keep sentences short", chunking="sentence")
        self._limit = limit

    async def _stream_validate(
        self, chunk: str, *, backend: Backend, ctx: Context
    ) -> PartialValidationResult:
        words = len(chunk.split())
        if words > self._limit:
            return PartialValidationResult(
                "fail", reason=f"sentence has {words} words (> {self._limit})"
            )
        return PartialValidationResult("unknown")

Sentence-level and word-level checks can now validate the same stream at once. Full version: per_requirement_chunking.py.

Register your own adapter

If you have trained your own aLoRA or PEFT adapter, you can now register it directly (#1619):

from mellea.backends.adapters import Adapter, Identity, LocalFileBinding, get_io_contract
from mellea.backends.adapters.catalog import AdapterType
from mellea.backends.huggingface import LocalHFBackend

backend = LocalHFBackend(model_id="ibm-granite/granite-4.1-3b")
backend.add_adapter(
    Adapter(
        identity=Identity(name="custom-failure-check", adapter_type="alora"),
        io_contract=get_io_contract("custom-failure-check"),
        weights=LocalFileBinding(
            name="custom-failure-check",
            adapter_type=AdapterType.ALORA,
            repo_id="your-org/my-adapter",
            revision="main",  # or a commit SHA to fix it to one version
        ),
    )
)

Two things to get right. revision has to be set, because a custom name has no catalog entry to fall back on; "main" follows the latest commit, and a full commit SHA fixes it to one version. And match the base model_id to whatever your adapter was trained against. Granite 4.1 is the current base for adapter work, since the public catalogs have no 4.2 weights yet. Note also that Identity takes adapter_type as a plain string while the binding takes the AdapterType enum. The walkthrough is tutorial 07, with tutorial 08 on schema migrations.

Adapters also reach further this release: embedded adapters work on LocalHFBackend (#1593) and Ollama gained adapter functions (#1634), so intrinsics that previously needed a vLLM-served model now run locally.

Granite 4.2 by default

Granite 4.2 3B is the new default local text model for start_session, OllamaModelBackend and LiteLLMBackend, with granite-4.2-3b, -8b and -30b identifiers available for Hugging Face and Ollama (#1587). It thinks by default, so ModelOption.THINKING: False now sends reasoning_effort="none" on OpenAI-compatible and LiteLLM backends, scoped to self-hosted targets because OpenAI's own reasoning models reject that value.

Two exceptions: adapter functions stay on Granite 4.1, because the public adapter catalogs publish no 4.2 weights yet, and vision stays on Granite Vision 4.1. This release updates the dense text model only.

What your traces show now

Past the renames listed under breaking changes, two things become visible that were not before. Tool execution gets its own spans (#1430), so a tool-calling loop shows individual calls rather than one opaque generation. And sampling and validation are traced (#1488), so an instruct-validate-repair cycle appears as a trace instead of something you reconstruct from logs. Streaming latency is now measured at provider receipt (#1631), so time-to-first-chunk reflects the provider rather than Mellea's own overhead.

Also worth knowing

m serve can now use the model name your OpenAI client sends rather than overriding it, if your served function declares a client_options parameter to receive it; routing on that value is up to your code (#1512). call_tools is public, so you can drive a tool loop yourself (#1544). Tool calls survive multi-turn history more reliably, and tool results now reach OpenAI-compatible providers in the shape those providers expect. Several errors are more specific than they were, including validation parse failures and adapter schema mismatches, which used to fail obscurely.

The rest is maintenance you get for free: imports are faster, resource leaks on backend clients and worker threads are fixed, and the langchain-core and litellm floors were raised to clear a CVE (#1447). Docs gained guidance on choosing between validation approaches (#1540) and a rewritten aLoRA example on the intrinsics API.

Breaking changes

These are the ones most people hit, usually one or two of them.

What changedWho is affectedWhat to doPR
stream_with_chunking() becomes stream(), consumed with async for on your own task. chunking defaults to None, not "sentence". Strategy classes renamed ...Chunker to ...Chunking, now in mellea.core.chunking.Streaming with validationFollow the migration guide#1543
Span attributes and client metrics renamed to the OpenTelemetry GenAI conventions. Old names removed, no dual-emit.Every dashboard, alert and queryRebuild queries from the observability docs#1551
ModelOutputThunk.tool_calls is a list, not a dict keyed by tool name. The dict silently dropped parallel calls to one tool.Anyone reading tool_calls by keyIterate it. Repeat calls are now visible#1435
Direct act()/aact() calls with requirements= and strategy=None raise ValueError. Those checks never ran. instruct() forwards them only when a strategy exists, so it is unaffected.Direct act()/aact() callersAdd a strategy, or attach the requirements to the action#1468
On LocalHFBackend, load_adapter() and unload_adapter() are now load_peft_adapter() and unload_peft_adapter(). list_adapters() returns registered, not loaded, adapters.Direct callers of adapter verbsRename both. For Granite Switch use EmbeddedBinding.apply_activation()#1422
m fix async is gone. m fix genslots is unaffected.Scripts or CI calling itDrop it, or run it from v0.7.0 first#1537
Per-chunk streaming telemetry moved onto the backend span, and is opt-in.Consumers of streaming telemetrySet MELLEA_GENERATION_CHUNK_EVENTS=true#1496

Smaller breaks, if you subclass or instrument Mellea: Requirement.stream_validate() is now @final, so a custom requirement overrides _stream_validate() instead (#1543); QuickCheckEvent.results holds PartialValidationSummary rather than PartialValidationResult (same PR); the public record_* telemetry helpers take a required operation argument (#1551); and the deprecated rag.check_context_relevance() is removed (#1579).

Telemetry has now been renamed two releases running, after v0.7.0's MELLEA_TRACE_* to MELLEA_TRACES_* change. Tracing is pre-1.0 and moving towards the OpenTelemetry spec, so expect one more settling release before treating these names as fixed.

Upgrading

pip install --upgrade mellea

Check breaking changes first. The two worth most of your attention are streaming, which has a full migration guide, and telemetry, where the old names are gone rather than deprecated.

The full release notes list all 134 PRs, including first contributions from nine new people.