HTTP API and Python SDK
Corresponding source
| Subject | Source |
|---|---|
| HTTP routes and OpenAI-compatible response | nanobot/api/server.py::handle_chat_completions, create_app |
| Python facade | nanobot/nanobot.py::Nanobot |
| SDK clients | nanobot/sdk/clients.py, nanobot/sdk/runtime.py |
| SDK streaming | nanobot/sdk/streaming.py::RunStream, SDKStreamingHook |
| SDK types | nanobot/sdk/types.py |
Learning objectives
After this lesson, you should be able to:
- explain how the HTTP API and SDK reuse
process_direct(); - describe the API Session lock and SSE lifecycle;
- distinguish SDK
run,run_streamed, andstream; - decide whether a problem belongs to an entry adapter or the Agent core.
Prerequisites
- Understand the AgentLoop direct path.
- Know the basic ideas of HTTP JSON, SSE, and Python async iterators.
1. HTTP API
Implementation:
nanobot/api/server.py- built with
aiohttp.web.
Primary endpoints:
| Endpoint | Purpose |
|---|---|
POST /v1/chat/completions | normal or streaming conversation |
GET /v1/models | current model information |
GET /health | health check |
2. Request parsing
Chat Completions supports:
- JSON;
- multipart/form-data;
- text;
- media;
session_id;- model override;
stream.
The input layer converts an OpenAI-compatible request into nanobot direct-call arguments. It does not call the Provider SDK directly.
3. API Session
Default:
api:defaultWith a session_id:
api:<session_id>The API server maintains:
session_locks: dict[str, asyncio.Lock]Requests within one API Session are serialized, while different Sessions can run concurrently. AgentLoop also has a Session lock; the API-layer lock protects the complete HTTP request and SSE-adapter lifecycle.
4. Non-streaming API
sequenceDiagram participant C as HTTP client participant A as API server participant L as AgentLoop C->>A: POST JSON A->>A: Parse + Session lock A->>L: process_direct L-->>A: response A->>A: Convert to chat.completion A-->>C: JSON
The API normalizes the process_direct() result into assistant text and wraps it in an OpenAI-compatible response with usage.
5. SSE
Streaming mode:
- creates an SSE response;
- registers a delta callback as
on_stream; - runs
process_direct()in the background; - writes a
chat.completion.chunkfor each delta; - writes the finish chunk and
[DONE]after the run completes; - closes the connection.
The key fact is:
HTTP SSE cannot close when only
on_stream_endfires, because the Agent may continue with a Tool Call after a stream segment. It must wait for the completeprocess_direct()call to return.
6. API empty-response retry
The non-streaming path has entry-layer recovery for an empty response. When debugging one, distinguish:
- Runner’s empty-response recovery;
- AgentLoop’s normalized empty-response text;
- an additional direct call from the API layer;
- response-text normalization.
Do not add an unbounded retry at the outermost layer.
7. Python SDK
High-level class:
nanobot/nanobot.py::Nanobot
Construction:
bot = Nanobot.from_config(...)Internally:
Config
→ AgentLoop.from_config
→ Nanobot(loop)8. run()
run():
- acquires an SDK-side Session lock;
- constructs
process_directkeyword arguments; - accepts Session, Channel, chat, sender, and media;
- supports ephemeral execution;
- supports Hooks;
- supports per-run model/preset overrides;
- returns a normalized
RunResult.
Use it when the caller only needs the final result.
9. run_streamed()
It returns RunStream:
- a background Task runs the full Turn;
- a Queue receives
StreamEvent; - events include run started, text/reasoning/tool stream events, and run completed or failed;
- the caller can consume events while separately awaiting the final result;
aclose()cancels an unfinished run.
10. stream()
stream() is the simpler async iterator:
async for event in bot.stream("Hello"):
...It still calls run_streamed() internally and closes an unfinished run in finally.
11. Resource cleanup
After using the SDK, call:
await bot.aclose()or:
async with Nanobot.from_config(...) as bot:
...This releases MCP and other resources owned by AgentLoop.
API and SDK comparison
| Feature | HTTP API | Python SDK |
|---|---|---|
| Transport | HTTP JSON/SSE | Python call/async iterator |
| Core entry | process_direct | process_direct |
| Session | api:<id> | caller-specified; SDK default otherwise |
| Streaming | SSE chunks | typed StreamEvent |
| Errors | HTTP/JSON | Python exception + failed event |
| Lifecycle | managed by server | managed by host program |
| Best for | cross-language calls | Python embedding |
Debugging boundary
If CLI direct works:
- API 400: inspect the body parser;
- API stream hangs: inspect the SSE writer and full-Turn wait;
- SDK event is missing: inspect the emitter/adapter;
- both produce the wrong final answer: return to AgentLoop/Runner;
- Sessions cross: inspect entry-point keys and locks.
Source and tests
nanobot/api/server.pynanobot/nanobot.pynanobot/sdk/streaming.pynanobot/sdk/types.pynanobot/sdk/runtime.pytests/test_openai_api.pytests/test_api_stream.pytests/test_api_attachment.py- use
rg -n "Nanobot\\(" teststo find SDK behavior tests
Self-check
- Why does the API not call the Provider directly?
- Why must SSE wait for
process_direct()to return? - What do the API Session lock and AgentLoop lock protect separately?
- How are
run_streamed()andstream()related? - Why does the SDK need
aclose()?
Next step
Read WebSocket and WebUI.