feat(api): require api_key when binding to all interfaces (parity with WS gateway)
The OpenAI-compatible API server had no authentication option, unlike the
WebSocket gateway which already refuses wildcard binds without a token.
When bound to 0.0.0.0, any caller who could reach the port could drive
the agent with its default tool posture.
- Add api_key field to ApiConfig (schema.py).
- Add wildcard_host_requires_auth validator that rejects wildcard binds
without api_key, mirroring the WS gateway pattern.
- Add Bearer-token auth middleware to the API server (server.py).
/health remains unauthenticated.
- Replace the wildcard-host CLI warning with a hard error when api_key
is unset, and pass api_key to create_app.
Fixes#4490
@
The non-streaming retry called process_direct again with the same
content, persisting a duplicate user turn. Pass persist_user_message=False
so the retry recovers a response without re-recording the user message.
* fix(api): forward real LLM usage in /v1/chat/completions response
_chat_completion_response() hardcoded prompt_tokens/completion_tokens
to zero. Now reads agent_loop._last_usage (set by process_direct
after every LLM call) and forwards the actual prompt/completion counts.
Streaming path is unchanged; usage is only surfaced in non-streaming
responses for now.
Fixes#4309
* fix: use defensive getattr for _last_usage and add it to all test mock agents
- Use getattr(agent_loop, '_last_usage', None) in server.py for safety
- Add _last_usage = {} to mock agents in test_api_attachment.py and test_api_stream.py
- Prevents AttributeError/500 when mock agents don't have the attribute
* fix(api): preserve provider total usage
---------
Co-authored-by: michaelxer <michaelxer@users.noreply.github.com>
Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
The HTTP compression buffer in aiohttp held all SSE chunks until
the stream ended, making streaming appear batched instead of
incremental. SSE payloads are small and frequent, so compression
provides negligible benefit while breaking real-time delivery.
The streaming API currently logs backend exceptions but still emits the
same `finish_reason: "stop"` + `[DONE]` terminator used for successful
responses. That makes a failed streamed request look successful to
OpenAI-compatible clients.
This keeps the fix narrow: track whether the stream backend failed and
suppress the success terminator in that case. A regression test locks in
the expected behavior.
Constraint: Keep the non-streaming response path untouched
Constraint: Follow up on the known limitation called out during PR #3222 review without redesigning the SSE protocol
Rejected: Introduce a custom SSE error event shape in the same patch | expands API surface and review scope
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If explicit streamed error events are added later, keep them distinct from the success stop+[DONE] terminator to preserve client retry semantics
Tested: PYTHONPATH=$PWD pytest -q tests/test_api_stream.py /Users/jh0927/Workspace/nanobot-validation-artifacts-2026-04-18/test_api_stream_error_regression.py
Not-tested: Full repository test suite
Related: #3260
Related: #3222
Three fixes in the API upload handling:
1. Multipart uploads now prefix filenames with a UUID to prevent
overwrites when two requests upload files with the same name.
2. JSON image_url content blocks with remote HTTPS URLs now return
a 400 error instead of silently dropping the image.
3. Model validation runs for both JSON and multipart requests,
fixing an inconsistency where multipart bypassed the check.
Move extract_documents() to nanobot.utils.document as a reusable helper
and call it once in AgentLoop._process_message, the single entry point
for all message processing (API + all channels).
This replaces the previous API-only _extract_documents() in server.py,
ensuring Telegram, Feishu, Slack, WeChat, and all other channels also
benefit from automatic document text extraction.
Adds a configurable max_file_size guard (default 50 MB) to skip
oversized files gracefully, preventing unbounded memory/CPU usage
from channel-downloaded attachments.
- server.py: removed _extract_documents and related imports
- document.py: added extract_documents() with size limit
- loop.py: calls extract_documents() at the top of _process_message
- Tests updated: 70 related tests pass
Made-with: Cursor
ContextBuilder._build_user_content now only handles images (its original
responsibility). Document text extraction (PDF, DOCX, XLSX, PPTX) is
performed by the new _extract_documents() helper in server.py, called
before process_direct(). This keeps the core context builder free of
format-specific dependencies and makes the API boundary the single place
where uploaded files are pre-processed.
Tests updated to reflect the new responsibility boundary.
Made-with: Cursor
Keep the API file upload branch current with main, enforce the documented JSON base64 per-file limit, and avoid leaking document extraction error strings into user prompts.
Made-with: Cursor
Reject mismatched models and require a single user message so the OpenAI-compatible endpoint reflects the fixed-session nanobot runtime without extra compatibility noise.
Read serve host, port, and timeout from config by default, keep CLI flags higher priority, and bind the API to localhost by default for safer local usage.
Expose OpenAI-compatible chat completions and models endpoints through a single persistent API session, keeping the integration simple without adding multi-session isolation yet.