Recipe Serve
HTTP server for recipe endpoints
Recipe Serve
The praisonai serve recipe command starts an HTTP server that exposes recipe endpoints for remote invocation.
Quick Start
# Start server on default port (8765)
praisonai serve recipe
# Start on custom port
praisonai serve recipe --port 8000
# Start with authentication
praisonai serve recipe --auth api-keyCommand Options
| Option | Description | Default |
|---|---|---|
--port <num> | Server port | 8765 |
--host <addr> | Server host | 127.0.0.1 |
--auth <type> | Auth type: none, api-key, jwt | none |
--api-key <key> | API key for authentication | – |
--reload | Enable hot reload (dev mode) | false |
--preload | Preload all recipes on startup | false |
--recipes <list> | Comma-separated recipe names to serve | all |
--config <path> | Path to serve.yaml config file | – |
Security
Host Binding Safety
By default, the server binds to 127.0.0.1 (localhost only). Binding to 0.0.0.0 (all interfaces) requires authentication.
# This will be REFUSED (no auth on public interface)
praisonai serve recipe --host 0.0.0.0
# This works (auth enabled)
praisonai serve recipe --host 0.0.0.0 --auth api-keyAuthentication Modes
Supported Auth Modes
The recipe server accepts exactly three auth modes, defined as a single source of truth in praisonai.recipe.serve:
SUPPORTED_AUTH_TYPES = frozenset({"none", "api-key", "jwt"})create_app(config) validates the auth value at construction time and raises ValueError for anything else — including typos (apikey, apiKey), empty strings (""), and non-strings (False, []).
from praisonai.recipe.serve import create_app
# Rejected: raises ValueError
create_app({"auth": "apikey"})
# ValueError: Unsupported recipe server auth mode 'apikey'.
# Expected one of: api-key, jwt, none
# Accepted
create_app({"auth": "api-key", "api_key": "my-secret-key"})Any non-localhost bind refuses to start unless auth is one of the supported non-none modes (or --api-key is passed on the CLI). The CLI runs its checks in this order:
- Load config
Read
serve.yaml(or CLI flags) into the config. - Validate auth mode
Reject any
authvalue outsideSUPPORTED_AUTH_TYPESbefore binding. - Bind guard
Refuse a non-localhost bind that resolves to
auth: nonewith no--api-key. - Serve
Start the server only after all checks pass.
API Key Authentication
# Start with API key auth
praisonai serve recipe --auth api-key --api-key my-secret-key
# Or use environment variable
export PRAISONAI_API_KEY=my-secret-key
praisonai serve recipe --auth api-keyClients must include the X-API-Key header:
curl -H "X-API-Key: my-secret-key" http://localhost:8765/v1/recipesConfiguration File
Create a serve.yaml file for persistent configuration:
# serve.yaml
host: 127.0.0.1
port: 8765
auth: api-key
api_key: your-secret-key # or use PRAISONAI_API_KEY env var
# Optional: limit which recipes are served
recipes:
- my-recipe
- another-recipe
# Optional: preload recipes on startup
preload: true
# Optional: CORS configuration
cors_origins: "*"Use the config file:
praisonai serve recipe --config ./serve.yamlConfiguration Precedence
- CLI flags (highest priority)
- Environment variables
- Config file
- Defaults (lowest priority)
API Endpoints
Health Check
GET /healthResponse:
{
"status": "healthy",
"service": "praisonai-recipe-runner",
"version": "2.7.1"
}List Recipes
GET /v1/recipes
GET /v1/recipes?tags=audio,videoResponse:
{
"recipes": [
{
"name": "my-recipe",
"version": "1.0.0",
"description": "Recipe description",
"tags": ["audio", "video"]
}
]
}Describe Recipe
GET /v1/recipes/{name}Response:
{
"name": "my-recipe",
"version": "1.0.0",
"description": "Recipe description",
"requires": {
"packages": [],
"env": ["OPENAI_API_KEY"]
},
"config_schema": {},
"outputs": []
}Get Recipe Schema
GET /v1/recipes/{name}/schemaResponse:
{
"name": "my-recipe",
"version": "1.0.0",
"input_schema": {},
"output_schema": []
}Run Recipe
POST /v1/recipes/run
Content-Type: application/json
{
"recipe": "my-recipe",
"input": {"query": "Hello"},
"config": {},
"options": {"dry_run": false}
}Response:
{
"ok": true,
"run_id": "run-abc123",
"recipe": "my-recipe",
"version": "1.0.0",
"status": "success",
"output": {"result": "..."},
"metrics": {"duration_sec": 1.5},
"trace": {
"run_id": "run-abc123",
"session_id": "session-xyz",
"trace_id": "trace-123"
}
}Stream Recipe (SSE)
POST /v1/recipes/stream
Content-Type: application/json
{
"recipe": "my-recipe",
"input": {"query": "Hello"}
}Response (Server-Sent Events):
event: started
data: {"run_id": "run-abc123", "recipe": "my-recipe"}
event: progress
data: {"step": "loading", "message": "Loading recipe..."}
event: progress
data: {"step": "executing", "message": "Running workflow..."}
event: completed
data: {"run_id": "run-abc123", "status": "success"}Streaming
praisonai serve recipe now streams when fronted by a gateway channel — the same DraftStreamer edits, status phrases, and SSE chunks a Python-Agent-backed bot produces.
praisonai serve recipe my_recipe --streamThe adapter maps each recipe event to a stream event the channel renders:
| Recipe event | Stream event emitted | Drives |
|---|---|---|
progress (with message) | TOOL_PROGRESS | Status / activity phrases during the turn |
output (with output) | FIRST_TOKEN + DELTA_TEXT | DraftStreamer edits + SSE chunks |
| end of iterator | STREAM_END | Finalises the draft |
error | raises (STREAM_END still emitted) | Falls back to failure-reply text |
Drive the async surface directly and subscribe to the adapter’s stream_emitter:
import asyncio
from praisonaiagents.gateway.adapters.recipe_adapter import RecipeBotAdapter
adapter = RecipeBotAdapter("my_recipe")
async def on_event(event):
print(event.type, getattr(event, "content", ""))
adapter.stream_emitter.add_callback(on_event)
async def main():
final = await adapter.astart("hello", stream=True)
print(final)
asyncio.run(main())Examples
Development Mode
# Start with hot reload for development
praisonai serve recipe --reloadProduction Mode
# Production with auth and preloading
praisonai serve recipe \
--host 0.0.0.0 \
--port 8000 \
--auth api-key \
--preloadUsing with Docker
FROM python:3.11-slim
RUN pip install praisonai[serve]
COPY serve.yaml /app/
WORKDIR /app
CMD ["praisonai", "recipe", "serve", "--config", "serve.yaml"]Client Examples
curl
# Health check
curl http://localhost:8765/health
# List recipes
curl http://localhost:8765/v1/recipes
# Run recipe
curl -X POST http://localhost:8765/v1/recipes/run \
-H "Content-Type: application/json" \
-d '{"recipe": "my-recipe", "input": {"query": "Hello"}}'
# With auth
curl -X POST http://localhost:8765/v1/recipes/run \
-H "Content-Type: application/json" \
-H "X-API-Key: my-secret-key" \
-d '{"recipe": "my-recipe", "input": {"query": "Hello"}}'Python
import requests
# Run recipe
response = requests.post(
"http://localhost:8765/v1/recipes/run",
json={
"recipe": "my-recipe",
"input": {"query": "Hello"}
},
headers={"X-API-Key": "my-secret-key"}
)
result = response.json()
print(result["output"])JavaScript
const response = await fetch("http://localhost:8765/v1/recipes/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "my-secret-key"
},
body: JSON.stringify({
recipe: "my-recipe",
input: { query: "Hello" }
})
});
const result = await response.json();
console.log(result.output);Environment Variables
| Variable | Description |
|---|---|
PRAISONAI_API_KEY | API key for authentication |
PRAISONAI_SERVE_HOST | Default host |
PRAISONAI_SERVE_PORT | Default port |
Troubleshooting
Port Already in Use
Error: [Errno 48] Address already in useSolution: Use a different port or stop the existing process:
praisonai serve recipe --port 8766
# Or
lsof -i :8765 | grep LISTEN | awk '{print $2}' | xargs killMissing Dependencies
Error: Serve dependencies not installed. Run: pip install praisonai[serve]Solution: Install serve extras:
pip install praisonai[serve]Auth Required for Public Binding
Error: Auth required for non-localhost binding. Use --api-key or set a supported auth mode (api-key, jwt) in the config.Solution: Enable authentication:
praisonai serve recipe --host 0.0.0.0 --auth api-keyUnsupported Auth Mode
ValueError: Unsupported recipe server auth mode 'apikey'. Expected one of: api-key, jwt, noneSolution: Use an exact supported mode (none, api-key, or jwt). Typos like apikey, api_key, or apiKey fail closed rather than downgrading to no auth.