Skip to main content

lmfn

pip install lmfn                 # functions and sessions
pip install "lmfn[verifiers]"    # + the verifiers harness, for RL and distillation

Status: alpha. Every example below runs, except where a section says otherwise. Offline tests: ./check. Live: tests/live.py passed on gpt-4.1-mini, claude-haiku-4-5, gemini-2.5-flash and qwen-2.5-72b. Training: docs/rl-walkthrough.md (a runnable walkthrough) and the examples examples/alphabet_sort_lmfn (multi-turn RL) and examples/banking77_lmfn (distilling a thinking teacher into a small model). Development setup: ./dev-venv (lmcc editable from ../lmcc).

The function is the prompt; calling it calls the model. lmfn is the thin layer that funnydspy, functai and elemai each prototyped: a typed Python function whose body is a model call. It stands on two libraries and adds only what they deliberately leave out:

layer answers library
wire what bytes go to which provider lm15 (router, providers, tools from functions)
spelling how each value is written into the prompt and read back lmcc (signature, adapter, bind, render, read, Turn)
control flow make the call; if the model asks for tools, run them and call again; return values lmfn
state what a later call remembers a session library, later, on top of lmfn

What lmfn owns and nothing else: which model (a default, overridable), the loop (one call, or the tool loop), and what you get back (the value, or everything). No prompt syntax, no parser, no provider code.

Working name lmfn.


1. A function that calls a model

import lmfn

lmfn.configure(model="gpt-4.1-mini")          # any lm15 model string

@lmfn.ai
def summarize(text: str) -> str:
    """Summarize the text in one sentence."""

summarize("LMCC is the calling convention for language models ...")
# 'LMCC defines how values are passed to and read back from language models.'

The parameters are the inputs, the return type is the output, the docstring is the instruction. No body, no sentinel: @lmfn.ai is @lmcc.fn plus a model. The call returns a plain str.

2. Types are the contract

from dataclasses import dataclass
from enum import Enum

class Sentiment(Enum):
    positive = "positive"
    negative = "negative"
    mixed = "mixed"

@dataclass
class Review:
    sentiment: Sentiment
    stars: int            # 1 to 5
    would_return: bool

@lmfn.ai
def read_review(text: str) -> Review:
    """Read the restaurant review."""

r = read_review("Great tacos, loud music. I'll be back.")
r.sentiment, r.stars, r.would_return        # (Sentiment.mixed, 4, True)

A dataclass return is several outputs; lmcc.One[Review] makes it one structured output instead. Everything lmcc checks, it checks here, and before any call: a type with no format refuses at the first call's bind, not at the provider.

3. Thinking first, without changing what you get back

@lmfn.ai(reasoning=True)
def solve(problem: str) -> int:
    """Solve the word problem."""

solve("Ana buys 7 pens at 3 each and pays with 50. Change?")    # 29

reasoning=True adds a hidden reasoning output. The value you get back is still the int. How the reasoning travels is chosen per model, by lmcc's choose: the provider's native thinking where the model has it, <think> tags otherwise. Same program on both.

4. Everything, when you want it

res = solve.call("Ana buys 7 pens ...")
res.value          # 29
res.values         # {'answer': 29, 'reasoning': '7 × 3 = 21, 50 − 21 = 29'}
res.repairs        # what the reader repaired (lmcc §4a), [] when clean
res.turn           # the lmcc.Turn: inputs, every model step and tool step, outputs
res.response       # the last lm15 Response (usage, finish reason, provider data)
res.usage          # summed over every model call of this turn

f(...) is f.call(...).value. One method, not a magic keyword: a keyword like functai's all=True collides with an input named all.

5. See the call before paying for it

req = solve.render("Ana buys 7 pens ...")   # the exact lm15 Request; no network
print(req.system)
solve.explain()                              # adapter, transports chosen, formats, stops

6. Tools: the loop, stated

def get_weather(city: str) -> str:
    """Current weather for a city."""
    return f"Sunny and 22°C in {city}."

@lmfn.ai(tools=[get_weather], max_steps=5)
def assistant(question: str) -> str:
    """Answer the question. Use a tool when you need facts you do not have."""

assistant("What's the weather in Montreal?")     # 'It is sunny and 22°C in Montreal.'

Tools are plain functions (lm15's derive_tool makes their schema). With tools, call runs the loop: render, call the model, and while the reply asks for tools, run them, record each result as a step of the same turn, call again. max_steps bounds it (default 8); reaching it raises lmfn.StepLimit with the turn so far. A tool that raises becomes a result the model sees ("error: ..."), unless tool_errors="raise".

What does not happen: the prompt style never changes because tools were added (functai silently switched to ReAct). Native tool calls where the model has them, fenced text calls otherwise, chosen by lmcc like reasoning. res.turn.steps shows every call and result.

7. Examples

@lmfn.ai(examples=[("I love it", Sentiment.positive), ("Awful.", Sentiment.negative)])
def classify(text: str) -> Sentiment:
    """Classify the sentiment."""

Each example becomes an lmcc example turn, written through the adapter like everything else. A tuple is (inputs, output) for a one-input function; a dict {"inputs": {...}, "outputs": {...}} in general.

8. Which model, and where settings live

lmfn.configure(model="gpt-4.1-mini", temperature=0.2)     # process default

with lmfn.configure(model="claude-haiku-4-5"):             # this block
    solve("...")

@lmfn.ai(model="gemini-2.5-flash", max_tokens=2000)        # this function
def draft(topic: str) -> str: ...

draft.using(model="gpt-4.1")("robots")                     # this call

Innermost wins: using > decorator > with block > configure. Settings are lm15 Config fields; unknown ones refuse. using returns a new function and never touches the original, so no per-call keyword can collide with an input name.

What the model can do (lmcc capabilities: instruct, native_function_calling, native_reasoning, stop_sequences) comes from the lm15 provider the model routes to and a small table of model names (lmfn/models.py), because lm15's model information does not say yet. OpenAI, Anthropic, Gemini and xAI get native tool calls; other hosts get text tool calls. Override when you know better: @lmfn.ai(capabilities={"native_reasoning": False}).

Thinking and the token budget. Some models think before answering by default (Gemini 2.5). That thinking counts against max_tokens. lmfn leaves the model's default alone (some models cannot turn thinking off); if a reply is cut off, the error says how many tokens went to thinking. With native reasoning, some providers keep the thinking text private, so res.values["reasoning"] can be empty while the answer is right.

9. Your own layout

xml = lmcc.adapter(messages=[
    lmcc.system("{instruction}\n{% for f in outputs %}<{f.name}>\n{f.value}\n</{f.name}>\n{% endfor %}"),
    lmcc.turns(), lmcc.user("{text}")])

@lmfn.ai(adapter=xml)
def summarize(text: str) -> str: ...

The default adapter is one lmfn ships, printed by explain(). Any lmcc adapter works, including one loaded from JSON.

10. When the reply is wrong

A reply lmcc cannot read raises its lmcc.Refusal (parse-missing-fields, parse-truncated, ...), with .partial. Retrying is a decision about money, so it is opt-in and visible:

@lmfn.ai(retries=1)
def extract(text: str) -> Review: ...

On a parse refusal, lmfn sends the reply back once with the refusal's hint ("reply is missing pattern section(s): 'stars'"), as a follow-up in the same turn. res.turn shows both attempts. parse-truncated retries with double max_tokens instead.

11. Streaming

s = solve.stream("...")
for event in s:                        # lmcc stream events, field by field
    ...
s.result                               # the same CallResult, after the loop

Not built yet: streaming a tool loop, and async (acall).

12. Sessions

chat = lmfn.Session(rag, forget=["context"], window=20)
chat("Who founded the bakery?", context=docs_1)
chat("And in what year?", context=docs_2)   # docs_1 is not sent again
chat.undo(); chat.fork(); chat.save("s.json"); lmfn.Session.load("s.json", rag)
chat.score(metric); chat.examples(min_score=0.5)   # training data

A session is a list of lmcc turns and a function. forget leaves inputs out of past turns (the current call still gets them; the record keeps them). The cost of forgetting, seen live: a fact that was only in a forgotten document is gone unless the model's answer repeated it. On 2026-09-23, gpt-4.1-mini answered "Marie Tremblay founded the bakery" without the year, so on the next turn it guessed a wrong year; Claude had written the year in its answer and kept it (tests/live_session.py).

13. Training: the verifiers harness

pip install "lmfn[verifiers]" adds the harness lmfn-verifiers: any @lmfn.ai function runs as a verifiers agent, with any lmcc adapter, and verifiers records the traces trainers use (prime-rl RL, SFT distillation).

vf-eval alphabet-sort-lmfn \
    --env.agent.harness.id lmfn-verifiers \
    --env.agent.harness.program alphabet_sort_lmfn.program:sort_names \
    --env.agent.harness.adapter original

Past replies are replayed verbatim, so a multi-turn episode stays one training sample; lmfn_verifiers.AnswerOnlyTask drops a teacher's reasoning before the trace is recorded, so a small student learns the answer only. See docs/rl-walkthrough.md.

14. Turns connect to the next layer

first = assistant.call("My name is Ana.")
second = assistant.call("What is my name?", turns=[first.turn])

A past turn is lmcc's record, re-spelled through the current adapter. Keeping, trimming, forking and saving a list of turns is the session library's job, built on exactly this.


Decisions taken (2026-09-23)

  1. Name. lmfn is a working name, not final.
  2. No hidden steps in the body (functai/elemai's thinking: str = _ai["..."]): it needs the body parsed as code and makes the body mean two things. reasoning=True and lmcc.Purpose in a dataclass cover it.
  3. Retries are opt-in (retries=0 by default): retrying spends money.
  4. Capabilities come from a small table in lmfn until lm15 carries them.

Release files for lmfn 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lmfn 0.1.0
File Size Uploaded
lmfn-0.1.0.tar.gz 34.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lmfn 0.1.0
File Interpreter ABI Platform
lmfn-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 60.4 kB

Release files / lmfn-0.1.0.tar.gz

Download URL lmfn-0.1.0.tar.gz
Size 34.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c10e9c303f272e892ba64c3945cb0f66f1feda0dc7114e3849740fe6ac334ed1
BLAKE2b-256 checksum
How to use checksums
6897e31e412a8ea72d9ba2946b84ea1ab82aa3d8025d930bc50a364dc380cd97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / lmfn-0.1.0-py3-none-any.whl

Download URL lmfn-0.1.0-py3-none-any.whl
Size 25.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a10115d9d730f19d6dff963c2cba087ffc6779b1862b998e2aecc42fc34401a6
BLAKE2b-256 checksum
How to use checksums
f7feaf12863240d824fe4136dc61214d74fad4564bb2b2a79c53223a3815f345
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page