generated from coulomb/repo-seed
feat: WP-0001 foundation + WP-0002 core extensions
WP-0001 — Foundation & GAAF Baseline - SCOPE.md, ARCHITECTURE-LAYERS.md, contracts/ tree - .claude/rules/ stubs filled (architecture, stack, boundary) - 57 tests (pytest), pyproject.toml with ruff+mypy, CI workflow WP-0002 — Core Extensions (FR-4 + FR-3) - FR-4: BudgetTracker (thread-safe) + LLMBudgetExceededError + optional RunConfig.budget_tracker + enforcement in all adapters - FR-3: async_execute_prompt on LLMAdapter ABC (asyncio.to_thread fallback) + native asyncio.create_subprocess_exec in ClaudeCodeAdapter 81 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,10 +5,11 @@ Implements abstraction layer for LLM integration, supporting
|
||||
multiple providers (OpenAI, Anthropic, local models, etc.).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any
|
||||
|
||||
from llm_connect.models import RunConfig, LLMResponse
|
||||
from llm_connect.models import RunConfig, LLMResponse, BudgetTracker
|
||||
|
||||
|
||||
class LLMAdapter(ABC):
|
||||
@@ -40,6 +41,26 @@ class LLMAdapter(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
async def async_execute_prompt(
|
||||
self,
|
||||
prompt: str,
|
||||
config: RunConfig,
|
||||
) -> LLMResponse:
|
||||
"""Execute a prompt asynchronously.
|
||||
|
||||
Default implementation runs :meth:`execute_prompt` in a thread
|
||||
executor so that the event loop is not blocked. Subclasses may
|
||||
override with a native ``asyncio``-based implementation.
|
||||
|
||||
Args:
|
||||
prompt: Compiled prompt text
|
||||
config: Execution configuration
|
||||
|
||||
Returns:
|
||||
LLMResponse with generated content
|
||||
"""
|
||||
return await asyncio.to_thread(self.execute_prompt, prompt, config)
|
||||
|
||||
@abstractmethod
|
||||
def validate_config(self, config: RunConfig) -> bool:
|
||||
"""
|
||||
@@ -53,6 +74,27 @@ class LLMAdapter(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
# ── Budget helpers (call in execute_prompt implementations) ─────
|
||||
|
||||
def _preflight_budget(self, config: RunConfig) -> None:
|
||||
"""Raise ``LLMBudgetExceededError`` if the budget is already exhausted."""
|
||||
if config.budget_tracker is not None and config.budget_tracker.remaining() == 0:
|
||||
from llm_connect.exceptions import LLMBudgetExceededError
|
||||
tracker = config.budget_tracker
|
||||
raise LLMBudgetExceededError(
|
||||
"Token budget exhausted before making request",
|
||||
total=tracker.total,
|
||||
spent=tracker.spent,
|
||||
requested=0,
|
||||
context={"total": tracker.total, "spent": tracker.spent},
|
||||
)
|
||||
|
||||
def _consume_budget(self, config: RunConfig, response: LLMResponse) -> None:
|
||||
"""Consume tokens from the budget tracker after a successful call."""
|
||||
if config.budget_tracker is not None:
|
||||
tokens = response.usage.get("total_tokens", 0)
|
||||
config.budget_tracker.consume(tokens)
|
||||
|
||||
|
||||
class MockLLMAdapter(LLMAdapter):
|
||||
"""
|
||||
@@ -88,11 +130,12 @@ class MockLLMAdapter(LLMAdapter):
|
||||
Returns:
|
||||
Mock LLMResponse
|
||||
"""
|
||||
self._preflight_budget(config)
|
||||
self.call_count += 1
|
||||
self.last_prompt = prompt
|
||||
self.last_config = config
|
||||
|
||||
return LLMResponse(
|
||||
response = LLMResponse(
|
||||
content=self.mock_response,
|
||||
model=config.model_name,
|
||||
usage={
|
||||
@@ -103,6 +146,8 @@ class MockLLMAdapter(LLMAdapter):
|
||||
finish_reason="stop",
|
||||
metadata={"mock": True},
|
||||
)
|
||||
self._consume_budget(config, response)
|
||||
return response
|
||||
|
||||
def validate_config(self, config: RunConfig) -> bool:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user