# Error Handling Patterns Analysis

## Overview

This document catalogs the current error handling patterns in the Kollabor CLI codebase, focusing on API communication, service-level errors, tool execution, and command handling. This analysis serves as the foundation for integrating OpenAI SDK error handling.

---

## Exception Hierarchy

### Current State

**IMPORTANT:** The codebase does NOT define custom exception classes. It relies entirely on Python's built-in exceptions:

- `Exception` - Generic exceptions (most common)
- `RuntimeError` - Runtime errors (service initialization, queue limits)
- `ValueError` - Invalid values (validation failures)
- `TypeError` - Type errors (invalid parameters)
- `asyncio.CancelledError` - Task cancellation
- `asyncio.TimeoutError` - Timeout on async operations
- `aiohttp.ClientError` - HTTP client errors (base class)
  - `aiohttp.ClientConnectionError` - Connection failures
  - `aiohttp.ServerDisconnectedError` - Server disconnected
  - `aiohttp.ClientPayloadError` - Payload encoding/decoding errors

### Exception Hierarchy Tree

```
Exception (built-in)
├── RuntimeError
│   └── Service initialization failures
│   └── Queue overflow (drop_newest strategy)
│   └── Task slot limits reached
│   └── HTTP session unavailable
├── ValueError
│   └── Missing required tags (response parser)
│   └── Message validation failures
├── TypeError
│   └── Invalid parameter types (LLM service)
├── asyncio.CancelledError
│   └── User cancelled requests
│   └── Streaming cancellation
├── asyncio.TimeoutError
│   └── Terminal command timeouts
│   └── MCP tool timeouts
└── aiohttp.ClientError
    ├── ClientConnectionError
    ├── ServerDisconnectedError
    └── ClientPayloadError
```

### User-Facing vs Internal

**User-Facing Errors:**
- `Exception` with API error messages (shown in terminal)
- `RuntimeError` for configuration issues (shown with context)
- `ValueError` for validation failures (shown as user error)

**Internal Errors:**
- `asyncio.CancelledError` (propagated internally)
- `aiohttp.ClientError` subclasses (logged, re-raised as generic `Exception`)
- `TypeError` (usually indicates bugs, logged)

---

## API Error Handling

### Current Implementation (Anthropic/OpenAI-Compat APIs)

The `APICommunicationService` handles API errors at multiple levels:

#### 1. HTTP Status Code Handling

**Location:** `core/llm/api_communication_service.py:484-521`

```python
if response.status == 200:
    # Success path
    ...

else:
    error_text = await response.text()
    error_msg = f"LLM API error: {response.status} - {error_text}"

    # Server errors trigger session recreation
    if 500 <= response.status < 600:
        logger.warning(f"Server error detected, recreating session: {error_msg}")
        await self._recreate_session()

    raise Exception(error_msg)
```

**Error Message Format:**
- `LLM API error: {status_code} - {error_text}`

**Status Code Ranges:**
- `200` - Success
- `400-499` - Client errors (auth, validation, rate limits) - Logged and raised
- `500-599` - Server errors - Triggers session recreation, then raised

#### 2. Network Error Handling

**Location:** `core/llm/api_communication_service.py:523-548`

**aiohttp ClientError Handling:**

```python
except aiohttp.ClientError as e:
    self._connection_stats['connection_errors'] += 1
    logger.error(f"API request failed with client error: {e}")

    # Session might be broken, recreate it
    if isinstance(e, (aiohttp.ClientConnectionError,
                     aiohttp.ServerDisconnectedError,
                     aiohttp.ClientPayloadError)):
        logger.info("Connection error detected, recreating session")
        await self._recreate_session()

    raise Exception(f"API connection error: {e}")
```

**Mapped Errors:**
- `ClientConnectionError` → "API connection error" + session recreation
- `ServerDisconnectedError` → "API connection error" + session recreation
- `ClientPayloadError` → "API connection error" + session recreation

**Timeout Handling:**

```python
except asyncio.TimeoutError:
    error_msg = f"LLM API timeout after {self.timeout} seconds"
    self._log_raw_interaction(payload, error=error_msg)
    logger.warning(f"API timeout, session may be stale")
    await self._recreate_session()
    raise Exception(error_msg)
```

#### 3. Adapter-Level Error Detection

**Location:** `core/llm/api_adapters/openai_adapter.py:153-175`

The OpenAI adapter detects format mismatches and configuration errors:

```python
def parse_response(self, raw_response: Dict[str, Any]) -> AdapterResponse:
    # Handle error responses
    if "error" in raw_response:
        error_msg = raw_response["error"].get("message", "Unknown error")
        logger.error(f"OpenAI API error: {error_msg}")
        return AdapterResponse(
            content=f"API Error: {error_msg}",
            stop_reason="error",
            raw_response=raw_response,
        )

    # Detect format mismatch (Anthropic response with OpenAI adapter)
    if "content" in raw_response and isinstance(raw_response.get("content"), list):
        if "choices" not in raw_response:
            logger.error("FORMAT MISMATCH: Got Anthropic response but using OpenAI adapter")
            return AdapterResponse(
                content="CONFIG ERROR: Your profile has tool_format='openai' but the server "
                       "returned an Anthropic-style response.\n\n"
                       "FIX: Run /profile, select this profile, press 'e' to edit, "
                       "change Tool Format to 'anthropic', then Ctrl+S to save.",
                stop_reason="format_error",
                raw_response=raw_response,
            )
```

**Special Error Cases:**
- API error object in response → "API Error: {message}"
- Format mismatch → User-friendly configuration instructions

---

## Error Recovery Patterns

### Session Recreation

**Triggered By:**
- Server errors (500-599)
- Connection errors
- Payload errors
- Timeouts

**Implementation:**

```python
async def _recreate_session(self):
    """Recreate the session after errors or timeout."""
    async with self._session_lock:
        try:
            logger.info("Recreating HTTP session")
            await self._cleanup_session()
            self._connection_stats['recreated_sessions'] += 1

            # Reinitialize with fresh session
            await self._create_session()

            logger.info("HTTP session recreated successfully")
        except Exception as e:
            logger.error(f"Failed to recreate session: {e}")
            raise
```

**Session Cleanup:**

```python
async def _cleanup_session(self):
    """Clean up session and connector resources."""
    try:
        if self.session and not self.session.closed:
            await self.session.close()
            await asyncio.sleep(0.1)  # Allow connections to close

        if self.connector:
            await self.connector.close()

        self.session = None
        self.connector = None
    except Exception as e:
        logger.error(f"Error during session cleanup: {e}")
```

### No Retry Logic

**Current State:** The codebase does NOT implement automatic retries.

**Error Accumulation:** The `ErrorAccumulator` utility (`core/utils/error_utils.py`) exists but is NOT used in the API communication layer.

**Retry Decorator Available:** The `retry_on_failure` decorator exists but is NOT applied to API calls.

### Circuit Breaker (Task Queue Only)

**Location:** `core/llm/llm_service.py:493-514`

The LLM service has a circuit breaker for task queuing (NOT for API calls):

```python
# Circuit breaker states
if self._circuit_breaker_state == "OPEN":
    timeout = self.task_config.queue.circuit_breaker_timeout
    time_since_failure = time.time() - self._circuit_breaker_open_time
    if time_since_failure < timeout:
        raise Exception(f"Circuit breaker OPEN - tasks rejected for {timeout - time_since_failure:.1f}s more")
    else:
        # Transition to HALF_OPEN
        self._circuit_breaker_state = "HALF_OPEN"
        logger.info("Circuit breaker transitioning to HALF_OPEN")

elif self._circuit_breaker_state == "HALF_OPEN":
    if self._test_task_running:
        raise Exception("Circuit breaker HALF_OPEN - test task already running")
```

**Purpose:** Prevents task queue overflow, NOT API-level resilience.

---

## User-Facing Messages

### Error Display Patterns

#### 1. API Error Messages

**Format:** Direct from API response

```python
error_msg = f"LLM API error: {response.status} - {error_text}"
```

**Examples:**
- "LLM API error: 401 - Unauthorized"
- "LLM API error: 429 - Rate limit exceeded"
- "LLM API error: 500 - Internal server error"

#### 2. Connection Error Messages

**Format:** Technical details exposed

```python
raise Exception(f"API connection error: {e}")
```

**Examples:**
- "API connection error: Cannot connect to host api.anthropic.com:443"
- "API connection error: Server disconnected"

#### 3. Timeout Messages

**Format:** User-friendly timeout duration

```python
error_msg = f"LLM API timeout after {self.timeout} seconds"
```

**Examples:**
- "LLM API timeout after 90 seconds"

#### 4. Configuration Error Messages

**Format:** Helpful instructions

```python
content="CONFIG ERROR: Your profile has tool_format='openai' but the server "
       "returned an Anthropic-style response.\n\n"
       "FIX: Run /profile, select this profile, press 'e' to edit, "
       "change Tool Format to 'anthropic', then Ctrl+S to save."
```

### Error Formatting Conventions

**Message Styles:**
- API errors: Prefix with "LLM API error:"
- Connection errors: Prefix with "API connection error:"
- Timeouts: Include duration in message
- Configuration errors: Use all-caps "CONFIG ERROR" prefix + "FIX:" instructions
- Tool errors: Include tool type and ID in message

**Logging Levels:**
- `logger.error()` - Critical failures (auth, connection)
- `logger.warning()` - Recoverable issues (timeout, session recreation)
- `logger.info()` - State changes (circuit breaker, session lifecycle)
- `logger.debug()` - Detailed diagnostics (request completion, parsing)

---

## Error Types Catalog

### Authentication Errors

**HTTP Status:** 401

**Current Handling:**
```python
if response.status == 200:
    # Success
else:
    error_msg = f"LLM API error: {response.status} - {error_text}"
    raise Exception(error_msg)
```

**User Message:** "LLM API error: 401 - {error_text from API}"

**Recovery:** None (user must fix API key)

**OpenAI SDK Mapping:** Will map to `AuthenticationError` or `OpenAIError`

### Rate Limit Errors

**HTTP Status:** 429

**Current Handling:**
```python
if response.status == 200:
    # Success
else:
    error_msg = f"LLM API error: {response.status} - {error_text}"
    raise Exception(error_msg)
```

**User Message:** "LLM API error: 429 - {error_text from API}"

**Recovery:** None (but session recreation happens on server errors)

**OpenAI SDK Mapping:** Will map to `RateLimitError`

### Network Errors

**Exception Types:**
- `aiohttp.ClientConnectionError`
- `aiohttp.ServerDisconnectedError`
- `aiohttp.ClientPayloadError`

**Current Handling:**
```python
except aiohttp.ClientError as e:
    self._connection_stats['connection_errors'] += 1
    logger.error(f"API request failed with client error: {e}")

    if isinstance(e, (aiohttp.ClientConnectionError,
                     aiohttp.ServerDisconnectedError,
                     aiohttp.ClientPayloadError)):
        logger.info("Connection error detected, recreating session")
        await self._recreate_session()

    raise Exception(f"API connection error: {e}")
```

**User Message:** "API connection error: {details}"

**Recovery:** Session recreation (automatic)

**OpenAI SDK Mapping:** Will map to `APIConnectionError` or `APITimeoutError`

### Timeout Errors

**Exception Type:** `asyncio.TimeoutError`

**Current Handling:**
```python
except asyncio.TimeoutError:
    error_msg = f"LLM API timeout after {self.timeout} seconds"
    self._log_raw_interaction(payload, error=error_msg)
    logger.warning(f"API timeout, session may be stale")
    await self._recreate_session()
    raise Exception(error_msg)
```

**User Message:** "LLM API timeout after {timeout} seconds"

**Recovery:** Session recreation (automatic)

**OpenAI SDK Mapping:** Will map to `APITimeoutError`

### Validation Errors

**HTTP Status:** 400

**Current Handling:**
```python
if response.status == 200:
    # Success
else:
    error_msg = f"LLM API error: {response.status} - {error_text}"
    raise Exception(error_msg)
```

**User Message:** "LLM API error: 400 - {error_text from API}"

**Recovery:** None (user must fix request)

**OpenAI SDK Mapping:** Will map to `BadRequestError` or `ValidationError`

### Tool Execution Errors

**Exception Type:** Generic `Exception` during tool execution

**Location:** `core/llm/tool_executor.py:151-166`

**Current Handling:**
```python
except Exception as e:
    import traceback
    error_details = f"Tool execution exception: {str(e)}\nTraceback: {traceback.format_exc()}"
    logger.error(f"Critical error during tool {tool_id} execution: {error_details}")
    result = ToolExecutionResult(
        tool_id=tool_id,
        tool_type=tool_type,
        success=False,
        error=f"Tool execution error: {str(e)}"
    )
```

**User Message:** "[{tool_type}] ERROR: Tool execution error: {details}"

**Recovery:** Continue with remaining tools (error is non-fatal)

**OpenAI SDK Impact:** None (tool errors are separate from API errors)

### Command Errors

**Exception Type:** Generic `Exception` during command execution

**Location:** `core/commands/executor.py:126-136`

**Current Handling:**
```python
except Exception as handler_error:
    error_result = CommandResult(
        success=False,
        message=f"Command /{command.name} failed: {str(handler_error)}",
        display_type="error",
        data={"error": str(handler_error)}
    )

    await self._emit_command_error(event_bus, command, "handler_error", error_result)
    self.logger.error(f"Command /{command.name} handler failed: {handler_error}")
    return error_result
```

**User Message:** "Command /{name} failed: {details}"

**Recovery:** None (command returns error result)

**OpenAI SDK Impact:** None (command errors are separate from API errors)

---

## Integration Notes

### What Needs to Change for OpenAI Errors

#### 1. Exception Hierarchy

**Recommended:** Create custom exception classes for better error categorization

```python
# Proposed: core/llm/exceptions.py
class KollaborAPIError(Exception):
    """Base class for API-related errors."""
    pass

class AuthenticationError(KollaborAPIError):
    """API authentication failed."""
    pass

class RateLimitError(KollaborAPIError):
    """API rate limit exceeded."""
    pass

class ValidationError(KollaborAPIError):
    """Request validation failed."""
    pass

class ServerError(KollaborAPIError):
    """Server error (5xx)."""
    pass

class NetworkError(KollaborAPIError):
    """Network/connection error."""
    pass

class TimeoutError(KollaborAPIError):
    """Request timeout."""
    pass
```

#### 2. OpenAI SDK Error Mapping

**OpenAI SDK Exceptions:**
- `openai.AuthenticationError` → Our `AuthenticationError`
- `openai.RateLimitError` → Our `RateLimitError`
- `openai.APIConnectionError` → Our `NetworkError`
- `openai.APITimeoutError` → Our `TimeoutError`
- `openai.APIError` (base) → Our `KollaborAPIError`
- `openai.BadRequestError` → Our `ValidationError`
- `openai.InternalServerError` → Our `ServerError`

**Mapping Strategy:**

```python
# Proposed: APICommunicationService integration
try:
    response = await openai_client.chat.completions.create(...)
except openai.AuthenticationError as e:
    raise AuthenticationError(f"Authentication failed: {e}")
except openai.RateLimitError as e:
    raise RateLimitError(f"Rate limit exceeded: {e}")
except openai.APIConnectionError as e:
    # Auto-recover with session recreation
    await self._recreate_session()
    raise NetworkError(f"Connection error: {e}")
except openai.APITimeoutError as e:
    await self._recreate_session()
    raise TimeoutError(f"Request timeout: {e}")
except openai.APIError as e:
    raise KollaborAPIError(f"API error: {e}")
```

#### 3. Retry Logic Integration

**Current State:** No retry logic

**OpenAI SDK Retry:** The SDK has built-in retry logic that can be configured

```python
from openai import OpenAI
import tenacity

# Configure retries
client = OpenAI(
    max_retries=3,
    timeout=30.0,
)

# Or use tenacity for custom retry logic
@tenacity.retry(
    stop=tenacity.stop_after_attempt(3),
    wait=tenacity.wait_exponential(multiplier=1, min=2, max=10),
    retry=tenacity.retry_if_exception_type(openai.APIConnectionError),
)
async def call_with_retry():
    return await client.chat.completions.create(...)
```

**Integration Approach:**
- Keep existing session recreation logic (it's robust)
- Add retry decorator for transient errors (timeouts, connection errors)
- Do NOT retry authentication/validation errors (4xx status codes)

#### 4. Error Message Compatibility

**Current Messages:**
- "LLM API error: {status} - {details}"
- "API connection error: {details}"

**OpenAI SDK Messages:**
- Exceptions include structured error details
- Need to extract user-friendly messages from SDK exceptions

**Proposed Message Format:**

```python
def extract_user_message(error: Exception) -> str:
    """Extract user-friendly message from SDK exception."""
    if isinstance(error, openai.AuthenticationError):
        return "Authentication failed: Check your API key"
    elif isinstance(error, openai.RateLimitError):
        return f"Rate limit exceeded: {error.message}"
    elif isinstance(error, openai.APIConnectionError):
        return f"Connection error: {error.message}"
    # ... etc
```

#### 5. Backward Compatibility

**Requirement:** Maintain support for Anthropic API and custom OpenAI-compatible endpoints

**Strategy:**
- Adapter pattern already exists (`BaseAPIAdapter`)
- Wrap OpenAI SDK in `OpenAIAdapter`
- Keep `AnthropicAdapter` using aiohttp
- Custom exceptions apply to both adapters

```python
class OpenAIAdapter(BaseAPIAdapter):
    def __init__(self, base_url: str = ""):
        super().__init__(base_url)
        # Initialize OpenAI client
        self.client = OpenAI(
            base_url=base_url,
            api_key=os.getenv("KOLLABOR_OPENAI_TOKEN"),
            max_retries=2,
        )

    async def call_api(self, payload: Dict[str, Any]) -> AdapterResponse:
        try:
            response = self.client.chat.completions.create(**payload)
            return self.parse_response(response)
        except openai.APIError as e:
            # Map to our custom exceptions
            raise map_openai_error(e)
```

---

## Error Flow Diagram

```
User Input
    ↓
LLMService.process_user_input()
    ↓
APICommunicationService.call_llm()
    ↓
[HTTP Request]
    ↓
┌─────────────────────────────────┐
│ Response Status Code Check      │
│                                 │
│ 200 → Success Path              │
│ 400-499 → Raise Exception       │
│ 500-599 → Session Recreate      │
│           + Raise Exception     │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│ Exception Catch Block           │
│                                 │
│ aiohttp.ClientError             │
│   → Session Recreate            │
│   → Raise "API connection error"│
│                                 │
│ asyncio.TimeoutError            │
│   → Session Recreate            │
│   → Raise "API timeout"         │
│                                 │
│ Generic Exception               │
│   → Log + Reraise               │
└─────────────────────────────────┘
    ↓
Error propagates to LLMService
    ↓
Displayed to user (via MessageDisplayService)
```

---

## Statistics and Monitoring

### Connection Statistics

**Location:** `APICommunicationService.get_connection_stats()`

**Tracked Metrics:**
```python
{
    'total_requests': int,
    'failed_requests': int,
    'recreated_sessions': int,
    'connection_errors': int,
    'failure_rate_percent': float,
    'connection_error_rate_percent': float,
    'active_connections': int,
    'available_connections': int,
    'session_age_seconds': float,
    'last_activity_age_seconds': float,
}
```

### Health Checks

**Location:** `APICommunicationService.health_check()`

**Health Checks:**
- Session status (initialized, closed)
- Connection test (minimal HTTP request)
- Resource health (failure rates)

**Health Thresholds:**
- Failure rate < 50%
- Connection error rate < 25%

---

## Recommendations for OpenAI SDK Integration

### 1. Create Custom Exception Hierarchy

**Priority:** HIGH

Create `core/llm/exceptions.py` with:
- Base `KollaborAPIError`
- Specific subclasses (Auth, RateLimit, Network, etc.)
- Map OpenAI SDK exceptions to our hierarchy

### 2. Enhance Error Messages

**Priority:** MEDIUM

Extract structured error details from OpenAI SDK:
- Type of error
- Human-readable description
- Suggested fixes (for configuration errors)
- Rate limit reset time (if available)

### 3. Add Retry Logic

**Priority:** HIGH

Use OpenAI SDK's built-in retry for transient errors:
- Timeouts
- Connection errors
- Server errors (500-599)

Do NOT retry:
- Authentication errors
- Validation errors
- Rate limit errors (wait instead)

### 4. Maintain Session Recreation

**Priority:** MEDIUM

Keep existing session recreation logic as fallback:
- Works even if SDK retry fails
- Handles connection pool exhaustion
- Compatible with both OpenAI SDK and aiohttp paths

### 5. Update Adapter Error Detection

**Priority:** HIGH

Add OpenAI SDK-specific error detection:
- SDK exception types
- Error response formats
- Format mismatch detection (already exists)

### 6. Testing Strategy

**Priority:** HIGH

Test error scenarios:
- Invalid API key → AuthenticationError
- Rate limit → RateLimitError (no retry)
- Network timeout → TimeoutError (with retry)
- Server error 500 → ServerError (with retry)
- Malformed response → FormatError

### 7. Backward Compatibility

**Priority:** CRITICAL

Ensure Anthropic API and custom endpoints still work:
- Adapters must raise same exception types
- Error messages must follow same format
- Session recreation must work for both

---

## Appendix: Error Message Examples

### Current User-Facing Messages

```
# Authentication
LLM API error: 401 - Unauthorized

# Rate Limit
LLM API error: 429 - Rate limit exceeded

# Connection Error
API connection error: Cannot connect to host api.example.com:443

# Timeout
LLM API timeout after 90 seconds

# Configuration Error
CONFIG ERROR: Your profile has tool_format='openai' but the server
returned an Anthropic-style response.

FIX: Run /profile, select this profile, press 'e' to edit,
change Tool Format to 'anthropic', then Ctrl+S to save.

# Tool Error
[terminal] ERROR: Command timed out after 90 seconds

# Command Error
Command /profile failed: Profile not found
```

---

## Document Metadata

**Generated:** 2025-01-14
**Analyzer:** Phase0AnalyzeErrors (GLM Agent)
**Scope:** API communication, service-level, tool execution, command errors
**Integration Target:** OpenAI SDK Integration
