--- phase: 04-single-model-qa plan: 01 type: execute --- Create AI client abstraction layer supporting Requesty and OpenRouter as model routers. Purpose: Establish the foundation for all AI model interactions - single queries, multi-model discussions, and consensus generation all flow through this client. Output: Working ai_client.py that can send prompts to any model via Requesty or OpenRouter. ~/.claude/get-shit-done/workflows/execute-phase.md ~/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-project-crud/03-03-SUMMARY.md # Key files: @src/moai/bot/config.py @src/moai/core/models.py # From discovery (no DISCOVERY.md needed - Level 1): # Both Requesty and OpenRouter are OpenAI SDK compatible: # - Requesty: base_url="https://router.requesty.ai/v1", model format "provider/model-name" # - OpenRouter: base_url="https://openrouter.ai/api/v1", needs HTTP-Referer header # Can use `openai` package with different base_url/headers **Tech available:** - python-telegram-bot, sqlalchemy, httpx, aiosqlite - pytest, pytest-asyncio **Established patterns:** - Service layer in core/services/ - Config loading from environment in bot/config.py - Async functions throughout **Constraining decisions:** - AI client as abstraction layer (PROJECT.md) - httpx for API calls (SPEC.md) Task 1: Add openai dependency and extend config pyproject.toml, src/moai/bot/config.py 1. Add `openai` to dependencies in pyproject.toml (unpinned per project standards) 2. Extend Config class in bot/config.py with: - AI_ROUTER: str (env var, default "requesty") - which router to use - AI_API_KEY: str (env var) - API key for the router - AI_REFERER: str | None (env var, optional) - for OpenRouter's HTTP-Referer requirement Note: Use existing pattern of loading from env with os.getenv(). No need for pydantic or complex validation - keep it simple like existing Config class. python -c "from moai.bot.config import Config; c = Config(); print(c.AI_ROUTER)" Config has AI_ROUTER, AI_API_KEY, AI_REFERER attributes; openai in dependencies Task 2: Create AI client abstraction src/moai/core/ai_client.py Create ai_client.py with: 1. AIClient class that wraps OpenAI AsyncOpenAI client: ```python class AIClient: def __init__(self, router: str, api_key: str, referer: str | None = None): # Set base_url based on router ("requesty" or "openrouter") # Store referer for OpenRouter # Create AsyncOpenAI client with base_url and api_key ``` 2. Async method for single completion: ```python async def complete(self, model: str, messages: list[dict], system_prompt: str | None = None) -> str: # Build messages list with optional system prompt # Call client.chat.completions.create() # Add extra_headers with HTTP-Referer if OpenRouter and referer set # Return response.choices[0].message.content ``` 3. Model name normalization: - For Requesty: model names need provider prefix (e.g., "claude" -> "anthropic/claude-sonnet-4-20250514") - For OpenRouter: similar format - Create MODEL_MAP dict with our short names -> full model identifiers - MODEL_MAP = {"claude": "anthropic/claude-sonnet-4-20250514", "gpt": "openai/gpt-4o", "gemini": "google/gemini-2.0-flash"} 4. Module-level convenience function: ```python _client: AIClient | None = None def init_ai_client(config: Config) -> AIClient: global _client _client = AIClient(config.AI_ROUTER, config.AI_API_KEY, config.AI_REFERER) return _client def get_ai_client() -> AIClient: if _client is None: raise RuntimeError("AI client not initialized") return _client ``` Keep it minimal - no retry logic, no streaming (yet), no complex error handling. This is the foundation; complexity comes later as needed. python -c "from moai.core.ai_client import AIClient, MODEL_MAP; print(MODEL_MAP)" AIClient class exists with complete() method, MODEL_MAP has claude/gpt/gemini mappings Before declaring plan complete: - [ ] `uv sync` installs openai package - [ ] Config loads AI settings from environment - [ ] AIClient can be instantiated with router/key - [ ] MODEL_MAP contains claude, gpt, gemini mappings - [ ] `ruff check src` passes - openai package in dependencies - Config extended with AI_ROUTER, AI_API_KEY, AI_REFERER - AIClient class with complete() method - MODEL_MAP with short name -> full model mappings - Module-level init_ai_client/get_ai_client functions - All code follows project conventions (type hints, docstrings) After completion, create `.planning/phases/04-single-model-qa/04-01-SUMMARY.md`