Model or dataset
koaning/smartfunc avatar
koaning/smartfunc

smartfunc: the @backend decorator that turns a Python function body into an LLM prompt

Turn docstrings into LLM-functions

516 stars15 forksPythonMIT

At a glance

What is it?
smartfunc is a small MIT-licensed Python library that wraps OpenAI SDK calls behind a decorator. The function body builds the prompt, the decorator makes the request, and Pydantic models can type the response. It is a thin binding, not a framework, and that is both its appeal and its ceiling.
Who is it for?
Adopt smartfunc if your prompts are already Python expressions and you want the OpenAI SDK's provider compatibility without writing a wrapper layer yourself. Skip it if you need retries, token accounting, streaming, or provider-agnostic clients, since none of those appear in the documented surface.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 93 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem smartfunc addresses is prompt plumbing, not prompt quality

Most Python code that calls an LLM ends up with the same three lines repeated: build a prompt string, call the client, pull the content out of the response object. smartfunc collapses that into a decorator. You write a normal function whose return value is the prompt, decorate it with @backend(client, model=...), and call it like any other function. The README's first example is a generate_summary(text: str) -> str function whose body is an f-string. After decoration, calling generate_summary(text) returns the model's answer as a string.

The audience is narrow on purpose. This is for someone who already has an OpenAI client object and wants the prompt-building step to live in Python rather than in a template file or a YAML config. The README makes that argument explicitly under the heading Full Python control, contrasting it with template syntax you would otherwise have to learn. If your team already writes prompts as Python strings, smartfunc removes boilerplate without asking you to learn a new abstraction. If your prompts live in files edited by non-engineers, the decorator model is the wrong shape, because the prompt is now code and only code can change it.

How the decorator routes a function call to the model

The mechanism described in the README is short enough to state in one sentence: your function can return either a string, which becomes the prompt, or a list of message dictionaries, which gives full conversation control, and the decorator handles calling the LLM and parsing the response. That is the whole architecture. There is no chain, no graph, no agent loop, no tool registry.

The two return types map onto two request shapes. Returning a string produces a single user message. Returning a list lets you construct the messages array yourself, which is how the README shows multi-turn chat: the example appends prior turns and the new user message into a list and returns it. One documented detail matters here. When you return a message list, the system parameter passed to the decorator is ignored. That is a sensible rule, since the list is the authoritative message array, but it is the kind of behaviour that silently produces the wrong prompt if you forget it.

Parameters such as temperature and max_tokens are declared on the decorator and forwarded to the SDK call. The system parameter becomes a system message when the function returns a string. Structured output is handled by passing a Pydantic model to response_format, and the README shows the decorated function returning an instance of that model, with attributes like result.summary and result.pros accessible directly. Parsing happens inside the decorator, which is the only place smartfunc adds logic beyond forwarding arguments.

Getting it installed and wired to a provider

The README gives one install command, using uv: uv pip install smartfunc. No version pin, no extras, no optional dependency groups are mentioned in the material supplied, so there is nothing to say about which OpenAI SDK version it expects.

Wiring it up takes an OpenAI client instance and a model name. The documented pattern is from smartfunc import backend, then from openai import OpenAI, then client = OpenAI(), then the decorator. Provider choice is delegated entirely to the SDK. The README states that the OpenAI SDK has support for many providers and names Ollama for local models and OpenRouter for cloud hosting, with the instruction to set api_key and base_url manually when constructing the client. The OpenRouter example passes api_key=os.getenv("OPENROUTER_API_KEY") and base_url="https://openrouter.ai/api/v1".

Async uses a separate import and a separate client class: from smartfunc import async_backend, from openai import AsyncOpenAI, and an async def function. The README warns directly that you may get throttled by the LLM provider if you send too many requests too quickly. There is no concurrency limiter described in the material, so that warning is a note about your responsibility, not a feature. Multimodal input is also manual: the README shows reading a file, base64 encoding it, and constructing a content list with a text part and an image_url part whose url is a data URI. Audio and video are mentioned as supported through the same base64 route, but no worked example for either appears in the supplied text.

What the documented surface does not cover

The README is a feature tour, and the gaps are as informative as the features. Nothing in the material describes retry behaviour, timeout handling, rate-limit backoff, token counting, cost tracking, streaming, or caching. The async section warns about throttling and stops there. If you need any of those, you are writing them around smartfunc rather than getting them from it.

There is also no error contract. The README says the decorator parses the response, and shows a Pydantic model coming back, but it does not say what happens when the model returns JSON that fails validation against that model. Whether the library retries, raises, or returns a partially populated object is not stated in the supplied material, and that is the single most important unknown for anyone using response_format in production. The same silence applies to malformed provider responses and to non-200 statuses.

One more boundary is worth naming. The README's own framing is that the library does one thing well, and the feature list is correspondingly flat: no prompt versioning, no evaluation harness, no tracing, no tool or function calling. A team that wants those things is not choosing between smartfunc and a heavier framework on features. It is choosing whether to add those layers itself.

The honest alternative is the SDK call you would have written anyway

The closest alternative to smartfunc is not another library. It is the plain OpenAI SDK, which is the dependency smartfunc already sits on. The difference is where the prompt-building code lives and how much indirection you accept. With the raw SDK, a summarise function is a client.chat.completions.create call with a messages list built inline, and the response content is extracted at the call site. With smartfunc, that same logic moves into a decorated function and the call site becomes an ordinary function call.

That is a real convenience for repeated call patterns, and it is close to zero benefit for a single call site. The trade is legibility: a reader who does not know the library sees a decorated function and has to know that the body is a prompt and the return value is a model response, not a string. The README's type hints make this worse in one respect, since the first example declares -> str while the function actually returns a prompt string and the decorated call returns a completion. The annotation describes the prompt, not the result.

A heavier alternative would be a framework that owns the prompt, the model config, and the execution graph. smartfunc deliberately does not do that. Whether that is better depends on whether you want the prompt to be a Python function or a versioned artifact, and the README takes a clear side: build prompts using Python, no template syntax to learn.

Maintenance cost and the MIT licence

The repository is MIT licensed, which permits commercial use, modification, and redistribution provided the copyright notice and permission notice are included. That is the standard permissive arrangement, and it means smartfunc can be vendored or forked without a licensing conversation. This is a description of the licence text, not legal advice; if your organisation has specific obligations around attribution in distributed binaries, that is a question for your legal team.

The maintenance picture is harder to assess from the supplied material. No releases were retrieved, so there is no version history to read and no changelog to check for breaking changes. The last push to the main branch is dated 2026-06-15, which indicates recent activity but says nothing about release discipline. Because the library is thin, the practical upgrade risk is low: the surface is two decorators and a set of pass-through keyword arguments, and the OpenAI SDK does the versioned work. The risk that actually matters is upstream, not here. A breaking change in the SDK's client interface or in how response_format is serialised would land on smartfunc's parsing code, and without release notes you would find out by running your tests rather than by reading a changelog.

For a library this small, that is an acceptable trade. It is also a reason to pin the dependency in your own project rather than tracking the main branch.

Who should take the dependency and what to check first

Take smartfunc if your prompts are already Python strings, you are comfortable with the OpenAI SDK as your provider interface, and you want the decorator to own request construction and response parsing. The library is at its best when you have several functions that follow the same shape and you are tired of repeating the same three lines. It is also a reasonable fit for Pydantic-typed extraction tasks, since response_format is a first-class argument and the README shows attribute access on the returned model.

Do not take it if you need streaming, retries with backoff, token accounting, or a provider abstraction that is not the OpenAI SDK. None of those are documented, and the async warning about throttling suggests you would be building the concurrency control yourself. Do not take it either if your prompts are edited by people who do not write Python, because the decorator model puts the prompt inside a function body.

Before adopting, verify two behaviours in your own environment. First, confirm that your chosen provider actually honours response_format with your Pydantic model, since the README's provider examples cover Ollama and OpenRouter for connection settings but not for structured output. Second, confirm the documented rule that returning a message list causes the system parameter to be ignored, and decide whether that is the behaviour you want in your chat paths. If both check out, the dependency is small enough to justify.

Editorial conclusion

Adopt smartfunc if your prompts are already Python expressions and you want the OpenAI SDK's provider compatibility without writing a wrapper layer yourself. Skip it if you need retries, token accounting, streaming, or provider-agnostic clients, since none of those appear in the documented surface. Before committing, verify two things in your own environment: that your provider returns a JSON body matching your response_format model, and that returning a message list really does suppress the system parameter as the README states.

Official sources

  1. Issues
  2. koaning/smartfunc on GitHub
  3. License: MIT
  4. README
Community notes

Community notes