Skip to content

Error Handling

This guide covers error responses, codes, and handling strategies for the Routstr API.

Error Response Format

All errors follow a consistent JSON structure:

{
  "error": {
    "type": "error_type",
    "message": "Human-readable error message",
    "code": "error_code",
    "details": {
      "additional": "context-specific information"
    }
  }
}

HTTP Status Codes

Status Meaning Common Causes
400 Bad Request Invalid parameters, malformed JSON
401 Unauthorized Invalid or missing API key
402 Payment Required Insufficient balance
403 Forbidden Access denied to resource
404 Not Found Endpoint or resource doesn't exist
422 Unprocessable Entity Validation errors
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server-side error
502 Bad Gateway Upstream API error
503 Service Unavailable Temporary outage

Error Types

Authentication Errors

Invalid API Key

{
  "error": {
    "type": "authentication_failed",
    "message": "Invalid API key provided",
    "code": "invalid_api_key"
  }
}

Status: 401
Resolution: Check API key format and validity

Expired API Key

{
  "error": {
    "type": "authentication_failed",
    "message": "API key has expired",
    "code": "key_expired",
    "details": {
      "expired_at": "2024-01-01T00:00:00Z",
      "refund_available": true
    }
  }
}

Status: 401
Resolution: Create new API key or contact admin for refund

Missing Authorization

{
  "error": {
    "type": "authentication_failed",
    "message": "Authorization header required",
    "code": "missing_auth"
  }
}

Status: 401
Resolution: Include Authorization: Bearer {api_key} header

Payment Errors

Insufficient Balance

{
  "error": {
    "type": "insufficient_balance",
    "message": "Insufficient balance for request",
    "code": "payment_required",
    "details": {
      "balance": 100,
      "required": 154,
      "shortfall": 54,
      "estimated_tokens": {
        "prompt": 50,
        "completion": 150
      }
    }
  }
}

Status: 402
Resolution: Top up API key balance

Invalid Token

{
  "error": {
    "type": "payment_error",
    "message": "Invalid Cashu token",
    "code": "invalid_token",
    "details": {
      "reason": "Token already spent"
    }
  }
}

Status: 400
Resolution: Use a valid, unspent token

Mint Unavailable

{
  "error": {
    "type": "payment_error",
    "message": "Cannot connect to Cashu mint",
    "code": "mint_unavailable",
    "details": {
      "mint_url": "https://mint.example.com",
      "retry_after": 60
    }
  }
}

Status: 503
Resolution: Try again later or use different mint

Validation Errors

Invalid Parameters

{
  "error": {
    "type": "invalid_request",
    "message": "Invalid request parameters",
    "code": "validation_error",
    "details": {
      "errors": [
        {
          "field": "temperature",
          "message": "Must be between 0 and 2",
          "value": 3.5
        },
        {
          "field": "model",
          "message": "Model 'gpt-5' not found",
          "value": "gpt-5"
        }
      ]
    }
  }
}

Status: 422
Resolution: Fix parameter values

Missing Required Fields

{
  "error": {
    "type": "invalid_request",
    "message": "Missing required fields",
    "code": "missing_fields",
    "details": {
      "missing": ["model", "messages"]
    }
  }
}

Status: 400
Resolution: Include all required fields

Rate Limiting

Rate Limit Exceeded

{
  "error": {
    "type": "rate_limit_exceeded",
    "message": "Too many requests",
    "code": "rate_limit",
    "details": {
      "limit": 100,
      "window": "1 minute",
      "retry_after": 45
    }
  }
}

Status: 429
Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 45

Resolution: Wait for retry_after seconds

Upstream Errors

Model Overloaded

{
  "error": {
    "type": "upstream_error",
    "message": "Model is currently overloaded",
    "code": "model_overloaded",
    "details": {
      "model": "gpt-4",
      "retry_after": 5
    }
  }
}

Status: 503
Resolution: Retry request after delay

Upstream Timeout

{
  "error": {
    "type": "upstream_error",
    "message": "Request to upstream API timed out",
    "code": "upstream_timeout",
    "details": {
      "timeout": 30,
      "endpoint": "chat/completions"
    }
  }
}

Status: 504
Resolution: Retry with shorter prompt or max_tokens

Content Policy

Content Filtered

{
  "error": {
    "type": "content_policy_violation",
    "message": "Content filtered due to policy violation",
    "code": "content_filtered",
    "details": {
      "reason": "harmful_content",
      "categories": ["violence", "hate"]
    }
  }
}

Status: 400
Resolution: Modify prompt to comply with policies

Error Handling Best Practices

Retry Logic

Implement exponential backoff with jitter:

import time
import random
from typing import Optional, Callable

def retry_with_backoff(
    func: Callable,
    max_retries: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 60.0
) -> Optional[Any]:
    """Retry function with exponential backoff."""

    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise

            # Check if error is retryable
            if hasattr(e, 'status_code'):
                if e.status_code in [429, 502, 503, 504]:
                    # Calculate delay with jitter
                    delay = min(
                        base_delay * (2 ** attempt) + random.uniform(0, 1),
                        max_delay
                    )

                    # Use retry_after if provided
                    if hasattr(e, 'retry_after'):
                        delay = e.retry_after

                    time.sleep(delay)
                else:
                    # Non-retryable error
                    raise

Error Categories

Group errors for handling:

class ErrorHandler:
    # Errors that should be retried
    RETRYABLE_ERRORS = {
        'rate_limit',
        'upstream_timeout',
        'model_overloaded',
        'mint_unavailable'
    }

    # Errors requiring user action
    USER_ACTION_ERRORS = {
        'insufficient_balance',
        'invalid_api_key',
        'key_expired'
    }

    # Errors requiring code changes
    CLIENT_ERRORS = {
        'validation_error',
        'missing_fields',
        'invalid_request'
    }

    @classmethod
    def handle_error(cls, error_response: dict) -> None:
        error_code = error_response['error']['code']

        if error_code in cls.RETRYABLE_ERRORS:
            # Implement retry logic
            pass
        elif error_code in cls.USER_ACTION_ERRORS:
            # Alert user
            pass
        elif error_code in cls.CLIENT_ERRORS:
            # Log for debugging
            pass

Graceful Degradation

Handle errors without breaking application flow:

async def get_ai_response(prompt: str) -> str:
    """Get AI response with fallback handling."""
    try:
        # Try primary model
        response = await client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        )
        return response.choices[0].message.content

    except InsufficientBalanceError:
        # Fall back to cheaper model
        try:
            response = await client.chat.completions.create(
                model="gpt-3.5-turbo",
                messages=[{"role": "user", "content": prompt}],
                max_tokens=100  # Limit tokens
            )
            return response.choices[0].message.content
        except Exception as e:
            logger.error(f"Fallback failed: {e}")
            return "Service temporarily unavailable"

    except Exception as e:
        logger.error(f"Unexpected error: {e}")
        return "An error occurred processing your request"

Logging Errors

Structure error logs for debugging:

import logging
import json

def log_api_error(error_response: dict, context: dict) -> None:
    """Log API errors with context."""
    logger = logging.getLogger(__name__)

    error_data = {
        'timestamp': datetime.utcnow().isoformat(),
        'error': error_response['error'],
        'context': {
            'endpoint': context.get('endpoint'),
            'api_key_id': context.get('api_key_id'),
            'request_id': context.get('request_id'),
            'model': context.get('model')
        }
    }

    logger.error(
        "API Error",
        extra={'structured_data': json.dumps(error_data)}
    )

User-Friendly Messages

Map technical errors to user messages:

ERROR_MESSAGES = {
    'insufficient_balance': "Your account balance is too low. Please add funds to continue.",
    'invalid_api_key': "Invalid API key. Please check your configuration.",
    'rate_limit': "Too many requests. Please wait a moment and try again.",
    'model_overloaded': "The AI service is busy. Please try again in a few seconds.",
    'validation_error': "Invalid request. Please check your input and try again."
}

def get_user_message(error_code: str) -> str:
    """Get user-friendly error message."""
    return ERROR_MESSAGES.get(
        error_code,
        "An unexpected error occurred. Please try again later."
    )

Common Scenarios

Handling Balance Errors

async def make_request_with_balance_check():
    try:
        # Check balance first
        balance_info = await client.get("/v1/wallet/balance")

        # Estimate cost
        estimated_cost = calculate_cost(model, prompt_length)

        if balance_info['balance'] < estimated_cost * 1.1:  # 10% buffer
            # Proactively top up
            await top_up_balance()

        # Make request
        return await client.chat.completions.create(...)

    except InsufficientBalanceError as e:
        # Handle insufficient balance
        shortfall = e.details['shortfall']
        await top_up_balance(amount=shortfall * 2)
        # Retry request

Handling Rate Limits

from datetime import datetime, timedelta

class RateLimitTracker:
    def __init__(self):
        self.reset_times = {}

    def is_limited(self, endpoint: str) -> bool:
        reset_time = self.reset_times.get(endpoint)
        if reset_time and datetime.now() < reset_time:
            return True
        return False

    def set_limit(self, endpoint: str, reset_timestamp: int):
        self.reset_times[endpoint] = datetime.fromtimestamp(reset_timestamp)

    def wait_time(self, endpoint: str) -> float:
        reset_time = self.reset_times.get(endpoint)
        if reset_time:
            return max(0, (reset_time - datetime.now()).total_seconds())
        return 0

Testing Error Handling

Unit Tests

import pytest
from unittest.mock import Mock

async def test_insufficient_balance_handling():
    # Mock API client
    mock_client = Mock()
    mock_client.chat.completions.create.side_effect = InsufficientBalanceError(
        required=100,
        available=50
    )

    # Test error handling
    handler = ErrorHandler(mock_client)
    result = await handler.safe_request(
        model="gpt-4",
        messages=[{"role": "user", "content": "test"}]
    )

    # Verify fallback behavior
    assert result.fallback_used is True
    assert result.model == "gpt-3.5-turbo"

Integration Tests

async def test_real_error_scenarios():
    # Test with invalid API key
    invalid_client = OpenAI(
        api_key="sk-invalid",
        base_url=test_url
    )

    with pytest.raises(AuthenticationError) as exc_info:
        await invalid_client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": "test"}]
        )

    assert exc_info.value.status_code == 401
    assert "invalid_api_key" in str(exc_info.value)

Monitoring Errors

Track error rates and patterns:

class ErrorMetrics:
    def __init__(self):
        self.error_counts = defaultdict(int)
        self.error_timestamps = defaultdict(list)

    def record_error(self, error_code: str):
        self.error_counts[error_code] += 1
        self.error_timestamps[error_code].append(datetime.now())

    def get_error_rate(self, error_code: str, window_minutes: int = 60) -> float:
        cutoff = datetime.now() - timedelta(minutes=window_minutes)
        recent_errors = [
            ts for ts in self.error_timestamps[error_code]
            if ts > cutoff
        ]
        return len(recent_errors) / window_minutes

Next Steps