Model 4: Remote Managed Runner
When to Use : Multi-tenant production environments, cloud deployments, or when you need centralized recipe management with authentication, rate limiting, and observability.
How It Works
The Remote Managed Runner is a production-grade deployment with authentication, rate limiting, metrics, and horizontal scaling.
Pros & Cons
Multi-tenant - Serve multiple clients with isolation
Centralized management - Single source of truth for recipes
Production-ready - Auth, rate limiting, metrics built-in
Scalable - Horizontal scaling with load balancer
Secure - TLS, API keys, JWT authentication
Observable - Prometheus metrics, distributed tracing
Network latency - Remote calls add latency
Operational complexity - Requires infrastructure management
Cost - Cloud resources, monitoring, etc.
Single point of failure - Unless properly distributed
Step-by-Step Tutorial
Deploy the Runner
docker run -d \
--name praisonai-runner \
-p 8765:8765 \
-e OPENAI_API_KEY= $OPENAI_API_KEY \
-e PRAISONAI_API_KEY=your-secure-key \
-e PRAISONAI_AUTH=api-key \
praisonai/runner:latest
Configure Authentication
# serve.yaml
host : 0.0.0.0
port : 8765
auth : api-key # or "jwt" for JWT authentication
api_key : ${PRAISONAI_API_KEY}
# JWT configuration (if using jwt auth)
jwt_secret : ${PRAISONAI_JWT_SECRET}
jwt_algorithm : HS256
Set Up Load Balancer
# nginx.conf
upstream praisonai {
least_conn ;
server runner1:8765;
server runner2:8765;
server runner3:8765;
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
location / {
proxy_pass http://praisonai;
proxy_http_version 1.1 ;
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection "upgrade" ;
proxy_set_header Host $ host ;
proxy_set_header X-Real-IP $ remote_addr ;
proxy_read_timeout 300s ;
}
}
Connect from Client
import os
from praisonai.endpoints import EndpointsClient
client = EndpointsClient(
base_url = "https://api.example.com" ,
api_key = os.environ[ "PRAISONAI_API_KEY" ]
)
# Check health
health = client.health()
print ( f "Server status: { health[ 'status' ] } " )
# Run recipe
result = client.invoke(
"my-recipe" ,
input = { "query" : "Hello" },
stream = False
)
print (result[ "output" ])
Production-Ready Example
import os
import logging
from typing import Any, Dict, Optional
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
logging.basicConfig( level = logging. INFO )
logger = logging.getLogger( __name__ )
class RemoteRecipeClient :
"""Production client for remote PraisonAI runner."""
def __init__ (
self ,
base_url : str ,
api_key : str ,
timeout : int = 60 ,
retries : int = 3
):
self .base_url = base_url.rstrip( "/" )
self .timeout = timeout
# Configure session with retries
self .session = requests.Session()
self .session.headers.update({
"Content-Type" : "application/json" ,
"X-API-Key" : api_key,
})
retry_strategy = Retry(
total = retries,
backoff_factor = 0.5 ,
status_forcelist = [ 500 , 502 , 503 , 504 ],
)
adapter = HTTPAdapter( max_retries = retry_strategy)
self .session.mount( "https://" , adapter)
self .session.mount( "http://" , adapter)
def health ( self ) -> Dict[ str , Any]:
"""Check server health."""
resp = self .session.get(
f " { self .base_url } /health" ,
timeout = 5
)
resp.raise_for_status()
return resp.json()
def run (
self ,
recipe_name : str ,
input_data : Dict[ str , Any],
config : Dict[ str , Any] = None ,
session_id : str = None ,
trace_id : str = None
) -> Dict[ str , Any]:
"""Run a recipe with full error handling."""
body = {
"recipe" : recipe_name,
"input" : input_data,
}
if config:
body[ "config" ] = config
if session_id:
body[ "session_id" ] = session_id
headers = {}
if trace_id:
headers[ "X-Trace-ID" ] = trace_id
try :
resp = self .session.post(
f " { self .base_url } /v1/recipes/run" ,
json = body,
headers = headers,
timeout = self .timeout
)
if resp.status_code == 401 :
raise PermissionError ( "Invalid API key" )
elif resp.status_code == 404 :
raise ValueError ( f "Recipe not found: { recipe_name } " )
elif resp.status_code == 429 :
raise RuntimeError ( "Rate limit exceeded" )
resp.raise_for_status()
return resp.json()
except requests.exceptions.Timeout:
logger.error( f "Request timeout for recipe { recipe_name } " )
raise
except requests.exceptions.ConnectionError as e:
logger.error( f "Connection error: { e } " )
raise
# Usage with environment-based configuration
if __name__ == "__main__" :
client = RemoteRecipeClient(
base_url = os.environ[ "PRAISONAI_ENDPOINTS_URL" ],
api_key = os.environ[ "PRAISONAI_ENDPOINTS_API_KEY" ],
timeout = 60 ,
retries = 3
)
# Health check
print (client.health())
# Run recipe
result = client.run(
"support-reply-drafter" ,
{ "ticket_id" : "T-123" , "message" : "I need help" },
trace_id = "req-12345"
)
print (result[ "output" ])
CLI Client
# Set environment variables
export PRAISONAI_ENDPOINTS_URL = https :// api . example . com
export PRAISONAI_ENDPOINTS_API_KEY = your-api-key
# Check health
praisonai endpoints health
# List available recipes
praisonai endpoints list
# Invoke a recipe
praisonai endpoints invoke my-recipe \
--input-json '{"query": "Hello"}' \
--json
# Stream output
praisonai endpoints invoke my-recipe \
--input-json '{"query": "Hello"}' \
--stream
Troubleshooting
Verify your API key: # Check if key is set
echo $PRAISONAI_ENDPOINTS_API_KEY
# Test with curl
curl -H "X-API-Key: $PRAISONAI_ENDPOINTS_API_KEY " \
https://api.example.com/health
Check network connectivity and firewall rules: # Test connectivity
curl -v https://api.example.com/health
# Check DNS
nslookup api.example.com
Implement exponential backoff: import time
def run_with_backoff ( client , recipe , input_data , max_retries = 5 ):
for attempt in range (max_retries):
try :
return client.run(recipe, input_data)
except RuntimeError as e:
if "Rate limit" in str (e):
wait = 2 ** attempt
time.sleep(wait)
else :
raise
raise RuntimeError ( "Max retries exceeded" )
For self-signed certificates in development: # NOT recommended for production
client.session.verify = False
# Better: Add your CA certificate
client.session.verify = "/path/to/ca-bundle.crt"
Security & Ops Notes
TLS everywhere - Always use HTTPS in production
API key rotation - Rotate keys regularly
Rate limiting - Protect against abuse
IP allowlisting - Restrict access by IP if possible
Audit logging - Log all API calls with trace IDs
Secrets management - Use vault/secrets manager for keys
# serve.yaml - Production security configuration
host : 0.0.0.0
port : 8765
auth : api-key
api_key : ${PRAISONAI_API_KEY}
rate_limit : 100 # requests per minute
max_request_size : 10485760
enable_metrics : true
enable_admin : false # Disable admin endpoints in production
# Observability
trace_exporter : otlp
otlp_endpoint : http://otel-collector:4317
Monitoring
# prometheus.yml
scrape_configs :
- job_name : 'praisonai-runner'
static_configs :
- targets : [ 'runner1:8765' , 'runner2:8765' , 'runner3:8765' ]
metrics_path : /metrics
Key metrics to monitor:
praisonai_recipe_duration_seconds - Recipe execution time
praisonai_recipe_total - Total recipe invocations
praisonai_recipe_errors_total - Error count
praisonai_active_sessions - Active sessions
Next Steps