Structured thinking as a pluggable layer for any OpenAI-compatible API
sotto voce — the passage spoken under the breath
| Getting Started | How It Works | Library | Proxy | Skills | Contributing |
Sotto gives any model a reasoning model's mechanism — a private thinking phase, effort control, interleaved thinking across tool calls, and skills — without changing your model, your SDK, or your code. The core is one stdlib-only file you can drop into any project. An optional proxy adds the same layer to any client just by repointing base_url.
Works with OpenAI · Azure OpenAI · vLLM · Ollama · DeepSeek · Groq · OpenRouter · LM Studio — anything that speaks /v1/chat/completions.
Latest Updates 🔥
- [2026/07] Initial public release of Sotto: single-file thinking engine, OpenAI-compatible proxy, and drop-in
SKILL.mdsupport.
Previous News
- Nothing here yet — you're early. ⭐ Star the repo to follow along.
About
Reasoning models (o-series, DeepSeek R1, gpt-5) share a set of mechanisms: they think privately before answering, scale that thinking by an effort setting, keep reasoning between tool calls, and hide the thinking from the final output. Sotto reproduces those mechanisms as a thin, portable layer so every model gets them — including the ones that don't reason natively.
| Reasoning-model concept | Sotto's implementation |
|---|---|
| Thinking blocks before the answer | <thinking> tags parsed out, or native reasoning_content harvested |
Adaptive thinking + effort (low/medium/high) |
ThinkingConfig.effort, mapped to reasoning_effort on o-series or a depth hint in the prompt |
Legacy budget_tokens |
ThinkingConfig.budget_tokens, auto-mapped to an effort tier |
| Interleaved thinking between tool calls | The run() loop: think → call tool → think about result → repeat |
| Thinking preserved during tool use | Raw assistant content (tags included) kept in message history across iterations |
| Thinking hidden from the user | Stripped from result.content; available in result.thinking and result.steps |
| Skills | SKILL.md folders, keyword-matched and injected into the system prompt per request |
Design goals
- Zero dependencies in the core.
thinking.pyis stdlib only. Drop it into any project, no install. - No lock-in. Any OpenAI-compatible client works. Repoint
base_urland you're done. - Progressive disclosure. Skills are injected only when a request matches, keeping context small.
- Graceful degradation. Weak models that skip the tags still produce a usable answer.
Getting Started
Install
pip install sotto-llm # core: zero dependencies, stdlib only
pip install "sotto-llm[proxy]" # + the proxy server (fastapi, uvicorn, openai, python-dotenv)
The core is still one self-contained stdlib-only file, so vendoring stays an option:
curl -O https://raw.githubusercontent.com/Madhav-Gohel/sotto/main/src/sotto_llm/thinking.py
Try it offline (no API key)
git clone https://github.com/Madhav-Gohel/sotto.git && cd sotto
pip install -e .
python example.py # scripted mock model runs the full think → tool → think → answer loop
How It Works
Sotto auto-selects one of three reasoning strategies based on the model:
visible_native— DeepSeek R1, QwQ, vLLM/Ollama reasoning models. The server already returnsmessage.reasoning_content; Sotto just harvests it.hidden_native— OpenAI o-series / gpt-5 family. Sotto passesreasoning_effort; reasoning happens server-side.prompted— everything else (gpt-4o-mini, llama, mistral…). Sotto injects a thinking protocol, the model reasons inside<thinking>tags, and Sotto parses them out. Unclosed tags (model cut off mid-thought) are handled.
Force a strategy with mode="prompted" | "native" | "off" instead of the default "auto".
Use as a Library
from openai import OpenAI
from sotto_llm import ThinkingEngine, ThinkingConfig
client = OpenAI(base_url="http://localhost:8000/v1") # vLLM, Ollama, Azure, anything
engine = ThinkingEngine(
client, "gpt-4o-mini",
config=ThinkingConfig(effort="high"),
tools=my_openai_tools,
tool_handlers={"get_weather": get_weather}, # python callables, run locally
skills_dir="skills",
)
result = engine.run([{"role": "user", "content": "weather in ahmedabad?"}])
result.content # final answer, thinking stripped
result.thinking # list of every thinking block, in order
result.steps # full interleaved trace: thinking / tool_call / tool_result / answer
result.as_openai_message() # {"role": "assistant", "content": ..., "reasoning_content": ...}
The engine runs a full think → act → think agentic loop, executing your tool handlers locally and feeding results back until the model produces a final answer (capped by max_tool_iterations).
Use as a Proxy
Any existing system — bots, agents, SDKs — gains thinking without code changes. Just repoint base_url.
pip install "sotto-llm[proxy]"
cp .env.example .env # set UPSTREAM_BASE_URL / UPSTREAM_API_KEY
uvicorn sotto_llm.proxy:app --port 8088
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8088/v1", api_key="x")
resp = client.chat.completions.create(
model="llama3.1",
messages=[{"role": "user", "content": "..."}],
extra_body={"thinking": {"effort": "high"}}, # optional; defaults apply without it
)
resp.choices[0].message.content # clean answer
resp.choices[0].message.reasoning_content # the thinking (DeepSeek/vLLM convention)
Reasoning comes back in message.reasoning_content — the same field DeepSeek and vLLM use — so existing clients keep working untouched. If the request includes tools, the proxy returns tool_calls to the caller like a normal OpenAI server (thinking still captured); the full agentic loop with local execution lives in library mode, since tools run on your side.
Skills
Each skill is a folder with a SKILL.md in the standard skill format: name and description in the frontmatter, instructions in the body. Existing skills in that format drop in unchanged.
---
name: code-review
description: Structured checklist for reviewing code changes... Use this skill
whenever the user asks you to review code, look at a PR or diff, or asks
whether some code "looks right" — even if they don't say "code review."
---
# Code Review
Review code in this order. The order matters: a fast, elegant function that
returns wrong answers is worse than a slow correct one...
The description does the triggering. Terms are extracted from the clause after "use this skill" — including quoted phrases and short domain tokens (pr, ci, sql) — then matched on word boundaries against the user's message. Only matching skills are injected, keeping context small (progressive disclosure).
keywords: a, b, c is supported as an optional extension. When present, it overrides description matching and a single hit triggers — useful when a skill needs to fire on jargon its description doesn't contain.
On matching quality: this is lexical matching, not semantic. It handles clear cases well but misses paraphrases that share no vocabulary with the description. If you have an embedding endpoint, swapping
match_skills()for cosine similarity over descriptions is a strict upgrade — the rest of the engine is unaffected.
Configuration Reference
| Field | Default | Meaning |
|---|---|---|
enabled |
True |
Turn the whole layer off with False |
effort |
"medium" |
low / medium / high reasoning depth |
budget_tokens |
None |
Legacy knob; <4096 low, <16384 medium, else high |
mode |
"auto" |
auto / prompted / native / off |
max_tool_iterations |
8 |
Safety cap on the agentic loop |
Proxy environment variables (via .env):
| Variable | Default | Meaning |
|---|---|---|
UPSTREAM_BASE_URL |
SDK default | The OpenAI-compatible server to forward to |
UPSTREAM_API_KEY |
x |
Key the upstream requires (any placeholder for local servers) |
SKILLS_DIR |
skills |
Directory the engine loads SKILL.md files from |
Limitations
- Prompted-mode thinking counts as normal output tokens (no separate billing lane like native reasoning).
- Tags are a convention, not enforcement; weak models occasionally skip them (the parser degrades gracefully — everything becomes the answer).
- The proxy is non-streaming; add SSE if you need it.
- The engine does not enforce a hard token cutoff on thinking; effort is a hint, matching how adaptive thinking treats it.
Contributing
Contributions are welcome — issues, feature requests, and PRs all help.
- Fork the repo and create a branch from
main. - Keep the core (
src/sotto_llm/thinking.py) dependency-free — stdlib only. New dependencies belong in the proxy extra or the examples. - If you touch matching or the reasoning loop, add or update a case in
example.pyso it runs offline. - Open a PR describing the change and the motivation.
Good first contributions: a streaming (SSE) proxy mode, an embedding-based match_skills() variant, and more example skills.
License
MIT — see LICENSE.
Citation
If you use Sotto in your work, please cite it:
@software{sotto,
title = {Sotto: Structured thinking as a pluggable layer for OpenAI-compatible APIs},
author = {Madhav Gohel},
year = {2026},
url = {https://github.com/Madhav-Gohel/sotto}
}
Built for anyone who wants their model to think first. ⭐ Star the repo if it's useful.
Metadata
Release files for sotto-llm 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sotto_llm-0.1.0.tar.gz | 14.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sotto_llm-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 27.9 kB
Release files / sotto_llm-0.1.0.tar.gz
| Download URL | sotto_llm-0.1.0.tar.gz |
|---|---|
| Size | 14.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b2992147a52dc939b75d5a5de27ffeadf5a7c5e2b75e2820093c4e12ad395710
|
|
BLAKE2b-256 checksum How to use checksums |
71cf09cd77968a97522992efd7b70e095cabf1a02c6b67ce6b207520f7c576a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
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 Jul 23, 2026.
Transparency logRelease files / sotto_llm-0.1.0-py3-none-any.whl
| Download URL | sotto_llm-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e71943454ddb1ce78f71363765e6b4cbaad18bf1072f7558b9bfe4e15eefbbf1
|
|
BLAKE2b-256 checksum How to use checksums |
7ec619326b97143fb37bfe3ccc37d638c5ed5d5ab6aae104a7ca40de1b4670da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
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 Jul 23, 2026.
Transparency log