Managoat is the hosted Fountain. Fountain is the open-source engine, and its name is on the CLI, the API, the SDK and this manual. Everything here applies to Managoat unless a page says it is for a self-hosted server.
OpenAI-compatible API
Fountain answers POST /v1/chat/completions, the request shape that every AI
gateway and every chat client with a base-URL field already speaks. The
model is a Fountain agent. Point LiteLLM, Portkey, Kong AI Gateway,
Cloudflare AI Gateway, Open WebUI, LibreChat, the openai SDK in any
language, or curl at https://your-fountain/v1 with an API key. The client
sees a model that answers in text. Behind it, an agent runs for as long as it
needs to in a sandbox of its own.
Open WebUI / LiteLLM / openai SDK ──HTTPS──▶ Fountain ──▶ sandbox: the Fountain agent
base URL = https://your-fountain/v1 POST /v1/chat/completions (its environment, vault, runtime)
model = pr-reviewer GET /v1/models
◀── chat.completion, or chat.completion.chunk as SSE
There is no plugin and no code on the client. The base URL, a key, and one header are the whole integration.
Summary
| Direction | Inbound. The client or gateway drives Fountain. |
| Talks over | OpenAI chat completions, at POST /v1/chat/completions and GET /v1/models. |
| Configured on | The client or the gateway. |
| Plugin | None. A URL is the whole integration. |
| Credential | A Fountain API key, as the bearer token. |
| Scope | One thread key is one conversation is one sandbox. |
| Status | Alpha. Behind the openai_compat flag, off by default on the hosted platform. Read Feature status. |
Alpha
On the hosted platform the flag is off by default. Ask us to turn it on for your account. Fountain answers
404with codeopenai_compat_not_enabledwhile it is off. The thread-key rule,reasoning_contentand the error codes can change between releases.
Set it up
Make an API key.
fountain keys create open-webui
Then give the client three things.
| Field | Value |
|---|---|
| Base URL | https://your-fountain/v1 |
| API key | ftn_... |
| Model | The agent's name, as GET /v1/models lists it. |
Most clients fetch GET /v1/models on save and fill their model picker with
your agents. If a client asks for a model name by hand, use the agent's name.
The agent's id works too.
From the openai Python SDK:
from openai import OpenAI
client = OpenAI(base_url="https://your-fountain/v1", api_key="ftn_...")
reply = client.chat.completions.create(
model="pr-reviewer",
messages=[{"role": "user", "content": "Review the open PRs on fountain."}],
extra_headers={"X-Fountain-Thread": "prs-2026-08-25"},
stream=True,
)
for chunk in reply:
print(chunk.choices[0].delta.content or "", end="")
From curl:
curl https://your-fountain/v1/chat/completions \
-H "Authorization: Bearer ftn_..." \
-H "X-Fountain-Thread: prs-2026-08-25" \
-H "Content-Type: application/json" \
-d '{"model": "pr-reviewer", "stream": true,
"messages": [{"role": "user", "content": "Review the open PRs on fountain."}]}'
A complete terminal chat on the openai package, with the model picker and
the thread header in place, is in the repository at
examples/openai-chat.
For LangChain and Deep Agents, where a Fountain agent is a subagent, read
LangChain and Deep Agents.
One thread is one conversation is one sandbox
Understand this part. Chat completions are stateless. The client sends the whole history on every call and expects the server to keep nothing. A Fountain conversation is the opposite. The sandbox holds the context, which is the agent's files, its shell history, and what it worked out last turn. Replay the transcript into it on each call, and you feed the agent its own words back.
So Fountain maps each chat to a conversation, and does not replay it. It
sends only the newest user message of each request as the prompt. The first
request on a thread opens a conversation. Each later request prompts the
conversation already bound to it.
The request has no field for a thread. Fountain reads the key from one of two places, in this order.
- The
X-Fountain-Threadheader. Set it to a stable id for the chat, such as the chat's own id in your client. Any gateway that forwards headers forwards this one. - The
userfield of the request, when the header is absent. Every SDK exposes it, and Open WebUI and LibreChat set it to the person's id. A client that cannot set headers gets one sandbox for each person and agent, which that person talks to for as long as they like. That is the same model as the team page.
Fountain refuses a request with neither, with a 400 that names the header. It does not fall back to one conversation for each message. That would spawn a sandbox for each line of a chat, and it is the failure mode this endpoint exists to avoid.
The key binds as channel openai:<key>, scoped to your account. Two accounts
whose clients mint the same key share nothing. The conversations appear in
Fountain like any others, and the API filters on the channel.
curl -H "Authorization: Bearer ftn_..." \
"https://your-fountain/api/conversations?channel_id=openai:prs-2026-08-25"
The system prompt
A client sends its system prompt on every call. Fountain delivers system
and developer messages once, with the first prompt of a new
conversation. After that the agent has it, and to repeat it each turn is
noise in the transcript and tokens on the bill.
That choice has a cost. Edit the system prompt in your client and it reaches a new thread. It does not reach a sandbox that already booted with the old one. Start a new thread to apply a system prompt you rewrote.
An agent's own system prompt still applies. It is the better place for whatever must hold on each surface.
Images
An image_url part on the newest user message becomes a prompt image, the
same as images on the real API. The URL must be a data: URL. Fountain does
not fetch a remote image on the client's behalf. Every chat client that
attaches a file inlines it as a data URL.
What comes back
| Fountain | Chat completions |
|---|---|
text blocks |
content |
thinking blocks |
reasoning_content |
tool_use and tool_result |
reasoning_content, one line for each call, such as → Bash and ← ok. |
The provision and setup stages |
reasoning_content, such as provision: started. |
A call to one of your tools |
tool_calls, then finish_reason: "tool_calls". Read Your tools. |
turn/done |
finish_reason: "stop" |
turn/failed |
An error object that carries the reason. |
With stream: true, the reply is SSE. Each event is a chat.completion.chunk
and the stream ends with data: [DONE], as OpenAI sends it. With
stream: false, the request blocks until the turn ends and answers with one
chat.completion. A turn can take minutes, so set the client's timeout with
that in mind, or stream.
reasoning_content is the field that Open WebUI, LibreChat and LiteLLM show
as the model's thoughts. A client that does not know the field ignores it.
Either way the bytes keep the client's stall watchdog fed while a fresh
sandbox provisions, which takes longer than a minute on some providers.
Two fields carry no information here, on purpose.
usageis always zeros. Fountain bills a turn in seconds, not tokens, and an invented token count is a number that a gateway would then add up.finish_reasonisstop, ortool_callswhen the agent waits on one of your tools. It is neverlength.
The sandbox's own tools never come back as tool calls. On this protocol a tool call means client, run this and send me the result. A Fountain agent ran its own tool, in its own sandbox, and the result is already in the text. The client sees what the agent said, not what it did. The tools that you define on the request are the exception, and the next section is about them.
The reply also carries a fountain object with the conversation_id, the
turn_id and the thread. Use them to reach the same conversation over the
real API.
Your tools
Send tools on the request, in OpenAI's function shape, and the agent gets
them beside its own. The agent does not know that they are remote. When it
calls one, the completion ends with the call and finish_reason: "tool_calls", and the turn waits. Run the tool, then send the next request
on the same thread with a role: "tool" message for each call. The turn
continues, and the completion ends with stop or with the next call. Every
agent framework already runs this loop. So a Fountain agent can be the model
inside create_agent, the openai SDK's tool runner, or Open WebUI's tool
servers.
1. POST /v1/chat/completions tools: [lookup_order] ──▶ the agent calls lookup_order
◀── finish_reason: "tool_calls", tool_calls: [{id: "call_1", function: {name, arguments}}]
2. POST /v1/chat/completions messages: [..., {role: "tool", tool_call_id: "call_1", content: "..."}]
◀── the rest of the turn, finish_reason: "stop"
From the openai Python SDK, the two requests are what
client.chat.completions.create returns and what you send back:
tools = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Find an order by id",
"parameters": {"type": "object", "properties": {"id": {"type": "string"}}},
}}]
headers = {"X-Fountain-Thread": "orders-1"}
messages = [{"role": "user", "content": "Where is order A-17?"}]
reply = client.chat.completions.create(model="support", messages=messages,
tools=tools, extra_headers=headers)
call = reply.choices[0].message.tool_calls[0]
messages += [reply.choices[0].message,
{"role": "tool", "tool_call_id": call.id, "content": lookup_order(**json.loads(call.function.arguments))}]
reply = client.chat.completions.create(model="support", messages=messages,
tools=tools, extra_headers=headers)
print(reply.choices[0].message.content)
Some rules.
- Fountain stores the tool names, descriptions and schemas on the
conversation. Send the same
toolson each request. A changed list replaces the old one at the next turn. - A call has a deadline, the same one as a permission prompt (five minutes by default). If you do not answer in time, the agent gets an error result that says so, and the turn continues without it.
- Fountain refuses a
usermessage while a call waits, with409and codetool_calls_pending. Answer the call first. - Fountain refuses a
role: "tool"message when nothing waits, with400and codeno_pending_tool_calls. The turn may have ended, or the call may have expired. tool_choice: "none"sends no tools for that request. Fountain refusesrequiredand a named tool with400, because it cannot force an agent's next action.- The agent can make more than one call at once. Fountain returns them one at a time. Answer each one, and the next comes back at once.
reasoning_contentcarries a line for each call, such as→ lookup_order (waiting for the caller), so a client that ignorestool_callscan see why the reply stopped.
The agent reaches your tools through one more Fountain-served MCP server in
its sandbox. A parked call blocks the agent's tool call for up to a minute,
then returns pending with a call_id, and the agent calls
wait_for_caller_result until you answer. Fountain reserves that name.
Errors
Errors that arrive before the reply use OpenAI's envelope, with the status that a client acts on.
| Status | code |
When |
|---|---|---|
| 400 | No thread key, no user message, or a remote image_url. |
|
| 401 | No key, or a bad one. | |
| 402 | insufficient_credits |
No credit on the account. |
| 404 | model_not_found |
No agent has that name or id in your account. |
| 409 | thread_busy |
The thread runs a turn now. Retry-After says when to send again. |
| 409 | tool_calls_pending |
The thread waits on your tool results. Answer them with role: "tool" messages. |
| 400 | no_pending_tool_calls |
The newest messages are tool results, but nothing waits for them. |
| 429 | sandbox_quota_exceeded |
Your concurrency cap. Terminate a conversation, or wait for one to idle out. |
Fountain refuses a second request on a thread that is mid-turn, and does not
queue it. Chat clients retry, and Retry-After tells them when.
A turn that fails after the reply started is an error event on the stream,
followed by [DONE]. With stream: false it is a 500 with code
turn_failed.
Model credentials
The agent needs a model credential here, as it does on each other surface. Add
one at /account/inference-credentials. Without one the sandbox spawns, the
session initialises, and the turn fails with Authentication required, which
reaches the client as an error that names it. Fountain has no platform-level
model key, on purpose
(ADR 0008).
What it does not do
- Structured outputs, logprobs,
n > 1,tool_choice: "required", or other features that assume the thing behind the URL is a model. - The Anthropic Messages shape. One dialect, and the OpenAI one is what gateways and clients speak.
- A model-provider abstraction for the sandbox side. To point the runtime at a gateway is an environment variable on the agent's environment.
The decision to carry a second public dialect on the server, and what it constrains, is ADR 0035.