Files
journal_organizer/.kiro/specs/claude-api-configuration/design.md
T
windyboy f7e54692a9 Initial project setup: Obsidian intelligent journal organizer
- Add core agent architecture with Command + Skill pattern
- Implement Claude API integration for content analysis
- Add Obsidian REST API integration for vault operations
- Create conversational interface (v2.0) with natural language processing
- Add comprehensive configuration management and validation
- Include project documentation and developer guides
- Set up testing framework with unit, integration, and property tests
- Add Kiro specs for Claude API configuration and code quality improvements
- Configure project steering files for development guidelines
2025-12-31 17:55:10 +08:00

16 KiB

Design Document

Overview

This design enhances the Claude API configuration system in the Obsidian journal organizer to support custom API URLs and comprehensive model selection. The enhancement maintains backward compatibility while providing flexibility for users to connect to different Claude API endpoints, including proxy servers, regional endpoints, and custom deployments. The design follows the existing configuration patterns and integrates seamlessly with the current Command + Skill architecture.

Architecture

The configuration enhancement will extend the existing YAML-based configuration system with:

  1. Flexible API URL Configuration: Support for custom Claude API endpoints
  2. Enhanced Model Validation: Comprehensive model name validation and suggestions
  3. Environment Variable Integration: Full support for environment-based configuration
  4. Backward Compatibility Layer: Seamless migration from existing configurations
  5. Configuration Validation Framework: Robust validation with helpful error messages

Components and Interfaces

Enhanced Claude Configuration Schema

from typing import Optional, Dict, Any, List
from pydantic import BaseModel, validator, Field
import re
from urllib.parse import urlparse

class ClaudeAPIConfig(BaseModel):
    """Enhanced Claude API configuration with URL and model validation"""
    
    api_key: str = Field(..., min_length=1, description="Claude API key")
    api_url: Optional[str] = Field(
        default="https://api.anthropic.com",
        description="Claude API base URL"
    )
    model: str = Field(
        default="claude-3-5-sonnet-20241022",
        description="Claude model name"
    )
    max_tokens: int = Field(default=4096, ge=1, le=200000)
    temperature: float = Field(default=0.7, ge=0.0, le=1.0)
    
    @validator('api_url')
    def validate_api_url(cls, v: Optional[str]) -> str:
        """Validate API URL format and accessibility"""
        if v is None:
            return "https://api.anthropic.com"
        
        # Parse URL to validate format
        parsed = urlparse(v)
        if not parsed.scheme or not parsed.netloc:
            raise ValueError(f"Invalid URL format: {v}")
        
        if parsed.scheme not in ['http', 'https']:
            raise ValueError(f"URL must use http or https protocol: {v}")
        
        # Remove trailing slash for consistency
        return v.rstrip('/')
    
    @validator('model')
    def validate_model_name(cls, v: str) -> str:
        """Validate Claude model name and provide suggestions"""
        valid_models = [
            # Claude 3 series
            "claude-3-opus-20240229",
            "claude-3-sonnet-20240229", 
            "claude-3-haiku-20240307",
            # Claude 3.5 series
            "claude-3-5-sonnet-20241022",
            "claude-3-5-haiku-20241022",
            # Latest aliases
            "claude-3-opus-latest",
            "claude-3-sonnet-latest",
            "claude-3-haiku-latest",
            "claude-3-5-sonnet-latest",
            "claude-3-5-haiku-latest"
        ]
        
        if v not in valid_models:
            # Check if it follows Claude naming pattern
            claude_pattern = r'^claude-\d+(\.\d+)?-(opus|sonnet|haiku)(-\d{8}|-latest)?$'
            if not re.match(claude_pattern, v, re.IGNORECASE):
                suggestions = ", ".join(valid_models[:5])
                raise ValueError(
                    f"Invalid model name: {v}. "
                    f"Valid models include: {suggestions}. "
                    f"Model names should follow pattern: claude-X-Y-YYYYMMDD or claude-X-Y-latest"
                )
        
        return v

Configuration Loading and Environment Variable Support

import os
import re
from typing import Any, Dict
import yaml

class ConfigurationLoader:
    """Enhanced configuration loader with environment variable expansion"""
    
    @staticmethod
    def expand_environment_variables(config_dict: Dict[str, Any]) -> Dict[str, Any]:
        """Recursively expand environment variables in configuration"""
        
        def expand_value(value: Any) -> Any:
            if isinstance(value, str):
                # Handle ${VAR} and ${VAR:-default} patterns
                pattern = r'\$\{([^}]+)\}'
                
                def replace_env_var(match):
                    var_expr = match.group(1)
                    if ':-' in var_expr:
                        var_name, default_value = var_expr.split(':-', 1)
                        return os.getenv(var_name.strip(), default_value)
                    else:
                        var_name = var_expr.strip()
                        env_value = os.getenv(var_name)
                        if env_value is None:
                            raise ValueError(
                                f"Environment variable '{var_name}' is not set. "
                                f"Please set it or provide a default value using ${{{var_name}:-default}}"
                            )
                        return env_value
                
                return re.sub(pattern, replace_env_var, value)
            
            elif isinstance(value, dict):
                return {k: expand_value(v) for k, v in value.items()}
            elif isinstance(value, list):
                return [expand_value(item) for item in value]
            else:
                return value
        
        return expand_value(config_dict)
    
    @classmethod
    def load_config(cls, config_path: str) -> Dict[str, Any]:
        """Load and validate configuration with environment variable expansion"""
        try:
            with open(config_path, 'r', encoding='utf-8') as f:
                config_dict = yaml.safe_load(f)
            
            # Expand environment variables
            config_dict = cls.expand_environment_variables(config_dict)
            
            return config_dict
            
        except FileNotFoundError:
            raise ConfigurationError(f"Configuration file not found: {config_path}")
        except yaml.YAMLError as e:
            raise ConfigurationError(f"Invalid YAML in configuration file: {e}")
        except ValueError as e:
            raise ConfigurationError(f"Environment variable error: {e}")

Enhanced Claude API Client

import aiohttp
import ssl
from typing import Optional, Dict, Any
from contextlib import asynccontextmanager

class ClaudeAPIClient:
    """Enhanced Claude API client with configurable endpoint support"""
    
    def __init__(self, config: ClaudeAPIConfig):
        self.config = config
        self.base_url = config.api_url
        self.api_key = config.api_key
        self.model = config.model
    
    @asynccontextmanager
    async def create_session(self):
        """Create HTTP session with proper configuration"""
        headers = {
            'Authorization': f'Bearer {self.api_key}',
            'Content-Type': 'application/json',
            'User-Agent': 'journal-organizer/1.0'
        }
        
        # Configure SSL context for custom endpoints
        ssl_context = ssl.create_default_context()
        if self.base_url.startswith('https://localhost') or 'localhost' in self.base_url:
            ssl_context.check_hostname = False
            ssl_context.verify_mode = ssl.CERT_NONE
        
        connector = aiohttp.TCPConnector(ssl=ssl_context)
        
        async with aiohttp.ClientSession(
            connector=connector, 
            headers=headers,
            timeout=aiohttp.ClientTimeout(total=60)
        ) as session:
            yield session
    
    async def validate_connection(self) -> bool:
        """Validate API connection and credentials"""
        try:
            async with self.create_session() as session:
                # Try a simple API call to validate connection
                url = f"{self.base_url}/v1/messages"
                payload = {
                    "model": self.model,
                    "max_tokens": 10,
                    "messages": [{"role": "user", "content": "test"}]
                }
                
                async with session.post(url, json=payload) as response:
                    if response.status == 401:
                        raise APIError("Invalid API key or unauthorized access")
                    elif response.status == 404:
                        raise APIError(f"API endpoint not found. Check if {self.base_url} is correct")
                    elif response.status >= 400:
                        error_text = await response.text()
                        raise APIError(f"API error ({response.status}): {error_text}")
                    
                    return True
                    
        except aiohttp.ClientError as e:
            raise APIError(f"Connection error to {self.base_url}: {str(e)}")

Data Models

Configuration Migration Support

class ConfigurationMigrator:
    """Handle migration from legacy configuration formats"""
    
    @staticmethod
    def migrate_claude_config(config_dict: Dict[str, Any]) -> Dict[str, Any]:
        """Migrate legacy Claude configuration to new format"""
        claude_config = config_dict.get('claude', {})
        
        # Add default api_url if not present
        if 'api_url' not in claude_config:
            claude_config['api_url'] = "https://api.anthropic.com"
            
        # Ensure model has a default value
        if 'model' not in claude_config:
            claude_config['model'] = "claude-3-5-sonnet-20241022"
        
        # Migrate old model names to new format if needed
        model_migrations = {
            "claude-3-sonnet": "claude-3-sonnet-20240229",
            "claude-3-opus": "claude-3-opus-20240229", 
            "claude-3-haiku": "claude-3-haiku-20240307"
        }
        
        if claude_config['model'] in model_migrations:
            old_model = claude_config['model']
            new_model = model_migrations[old_model]
            claude_config['model'] = new_model
            print(f"INFO: Migrated model '{old_model}' to '{new_model}'")
        
        config_dict['claude'] = claude_config
        return config_dict

Error Handling

Comprehensive Error Management

class ClaudeConfigurationError(ConfigurationError):
    """Specific error for Claude API configuration issues"""
    pass

class ModelValidationError(ClaudeConfigurationError):
    """Error for invalid model names with suggestions"""
    
    def __init__(self, message: str, suggestions: List[str] = None):
        super().__init__(message)
        self.suggestions = suggestions or []

class APIConnectionError(ClaudeConfigurationError):
    """Error for API connection issues"""
    pass

def validate_claude_configuration(config: Dict[str, Any]) -> ClaudeAPIConfig:
    """Validate Claude configuration with helpful error messages"""
    try:
        claude_config = config.get('claude', {})
        return ClaudeAPIConfig(**claude_config)
        
    except ValidationError as e:
        # Transform pydantic errors into user-friendly messages
        error_messages = []
        for error in e.errors():
            field = error['loc'][0] if error['loc'] else 'configuration'
            message = error['msg']
            
            if field == 'api_url':
                error_messages.append(
                    f"Invalid API URL: {message}. "
                    f"Please provide a valid HTTP/HTTPS URL like 'https://api.anthropic.com'"
                )
            elif field == 'model':
                error_messages.append(
                    f"Invalid model name: {message}"
                )
            else:
                error_messages.append(f"Invalid {field}: {message}")
        
        raise ClaudeConfigurationError(
            f"Claude configuration errors:\n" + "\n".join(f"- {msg}" for msg in error_messages)
        )

Testing Strategy

Dual Testing Approach

The testing strategy combines unit tests for specific functionality with property-based tests for comprehensive validation:

Unit Tests:

  • Test configuration loading with various URL formats
  • Test model name validation with valid and invalid inputs
  • Test environment variable expansion with different patterns
  • Test backward compatibility with legacy configurations
  • Mock API calls to test connection validation

Property-Based Tests:

  • Validate that all valid URL formats are accepted
  • Test that model name validation works across all valid model patterns
  • Verify environment variable expansion handles all supported formats
  • Ensure configuration migration preserves all existing functionality

Testing Framework:

  • Use pytest for unit testing framework
  • Use hypothesis for property-based testing
  • Configure tests to run minimum 100 iterations per property test
  • Tag each property test with: Feature: claude-api-configuration, Property {number}: {property_text}

Now I need to use the prework tool to analyze the acceptance criteria before writing the correctness properties:

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Configuration Field Support

For any configuration dictionary containing claude section fields (api_url, model), the system should successfully parse and store these values in the configuration object Validates: Requirements 1.1, 2.1

Property 2: URL Validation and Error Handling

For any URL string provided as api_url, the system should validate proper URL format, reject malformed URLs with specific error messages, and accept valid HTTP/HTTPS URLs Validates: Requirements 1.4, 1.5, 4.1

Property 3: Model Name Validation with Suggestions

For any model name string, the system should validate Claude naming conventions, accept all supported model variants, and provide helpful suggestions when invalid model names are provided Validates: Requirements 2.2, 2.3, 2.4, 4.2

Property 4: Default Value Behavior

For any configuration missing api_url or model fields, the system should use appropriate default values (default Anthropic API URL and sensible default model) without errors Validates: Requirements 1.2, 2.5

Property 5: Custom Configuration Usage

For any valid custom api_url or model specified in configuration, the system should use these values in API client initialization and requests Validates: Requirements 1.3

Property 6: Environment Variable Expansion

For any configuration value containing environment variable references, the system should correctly expand variables for both api_url and model fields, and validate expanded values the same as direct configuration Validates: Requirements 5.1, 5.2, 5.4, 5.5

Property 7: Environment Variable Error Handling

For any undefined environment variable referenced in configuration, the system should provide clear error messages indicating which variables are missing Validates: Requirements 5.3

Property 8: Backward Compatibility

For any legacy configuration format, the system should load successfully, maintain existing functionality, and provide informational messages about new options without breaking changes Validates: Requirements 3.1, 3.2, 3.3, 3.4

Property 9: API Connection Validation

For any API configuration (URL and credentials), the system should validate connectivity when possible and provide clear authentication error messages for invalid credentials Validates: Requirements 4.3, 4.4

Property 10: Comprehensive Error Messaging

For any configuration validation failure, the system should provide helpful error messages with corrective suggestions, appropriate logging, and references to documentation where applicable Validates: Requirements 4.5, 6.3, 6.4

Property-Based Testing Configuration

Each property test will be tagged with comments referencing the design document property and run with sufficient iterations to catch edge cases through randomization. Tests will use the hypothesis library for property-based testing with minimum 100 iterations per test.