reimplementation of the Claude Agent SDK that works with any model: local, open-source, or commercial. write agent code once and run it against Ollama, Groq, Together, Fireworks, or vLLM by changing one string. universal tool-use across models, session persistence, budget controls, MCP integration, and a terminal coding agent included.
one anthropic-shaped api, nine provider adapters, three tool-use paths
python
asyncio
anyio
httpx
msgspec
sse
sqlite
jsonl
mcp
json-schema
ollama
vllm
oauth
the problemThe agent loop is not the hard part. The wire formats are.
Anthropic's claude-agent-sdk is a good shape and welded to one vendor. Running the same program against a Qwen on my own GPU box means writing a second agent loop.
A field-renaming translation layer works for about a day. The three worlds do not just spell things differently, they disagree about whether tool calls exist as a channel at all.
mantis-agent-sdk
the PyPI distribution, currently 2.63.0
mantis_agent
the import package, and the drop-in for claude_agent_sdk
mantis
the bundled terminal coding agent, same engine
where a tool call livessse bytes to one normalised event type
the ideaResolve a (model, backend) pair to one tool-use path, once
Tool-calling ability belongs to the pair, not to either half. Qwen2.5 emits tool_calls through vLLM and cannot through a bare llama.cpp server, so I made both sides explicit and let one function pick.
The capability table is hand-maintained, which I treat as a feature: nobody publishes a machine-readable answer to whether a checkpoint emits well-formed tool calls, and probing at runtime costs a generation.
open-weight entries
38
hosted entries
24
family defaults
17, for anything unrecognised
three ways to get a tool call out of a modelcapability resolution, at Agent constructionpath A, a native tool call from request to result
how it worksOne turn, from a model name to a tool result
A caller passes model="qwen2.5:7b" and nothing else. The name's shape infers the backend, and profile matching is anchored on purpose: api.deepseek.com matches, a self-hosted box named deepseek-box does not.
On path B the tools are rendered into the system prompt one pretty-printed schema each. I minified that blob once and the failure rate went up, because weak models lose track of which properties belong to which tool.
one turn, end to endthe agent loop, one turn and the decision to take anotherpath B, where the parser sits in the stream
the hard partSmall models lie about tool calls in four different ways
The protocol is five lines of system prompt. Holding a 7B model to it across a long coding session is the whole problem, so most of path B is failure handling.
Truncation taught me the most. A call cut off at max_tokens and a call with a syntax error both arrive unparseable and need opposite advice, and giving the wrong one buys an infinite loop at a full generation per turn.
parser
two states, parses on close, buffers under a kilobyte
split tags
holds back up to 16 chars that prefix <tool_call>
escaping
tool output is escaped so it cannot forge a call
four ways a call comes back wrongthe repair ladder, tried in orderthe discriminator that decides what to tell the model
the stackWhat it runs on, and what I refused to depend on
No openai, no anthropic, no ollama client library. Every adapter is raw HTTP against the documented wire format, because a vendor SDK that validates model names against its own allowlist rejects exactly the self-hosted case this exists for.
Retries live in a custom httpx transport, so every provider gets them without knowing. It retries POST, which is normally wrong and is fine here: a completions request has no application-level side effect and no partly-read stream is ever replayed.
what sits on whatwhere the python isnine provider adapters, no vendor client libraries
parityThe argument the project is actually making
PARITY.md is notes taken against the live Claude Code docs and the deobfuscated CLI, because "a reimplementation" is a claim that has to be checkable. Some of it I copied outright, such as honouring the ultracode keyword only from human-typed input, since that is an injection boundary.
The narrow claim is the one I will defend: every canonical Claude Agent SDK Python example runs verbatim after changing the import line and the options class name. That is a test, not a slogan.
what parity claimswhat happens to one tool callwhat a turn writes, and what resume reads back
where it standsWhat works, and what I know is wrong
The three-path tool layer, sessions with fork and resume, MCP client and server with OAuth, and the mantis terminal on top all work. Most of the 6,878 tests run against a mock provider, so the wire-format work is testable without a GPU or an API key.
The thing I would rebuild first is the capability probe. Capability is declared today; it should be declared and then verified once per backend with a cheap handshake, cached on disk, so an unknown endpoint gets the good path without me shipping a release.
23 built-in tools, by what they touchhonest ledgeran external mcp server's tools reaching the registry