# Contract: Core — LLMAdapter Interface **Layer:** Core **Version:** 0.1.0 **Status:** Draft (stabilises at v1.0.0) **Last updated:** 2026-04-01 --- ## LLMAdapter ABC `llm_connect.adapter.LLMAdapter` ### Interface ```python class LLMAdapter(ABC): @abstractmethod def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse: ... @abstractmethod def validate_config(self, config: RunConfig) -> bool: ... ``` **Planned addition (WP-0002 T07):** ```python async def async_execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse: # Default: runs execute_prompt in a thread executor ... ``` ### Invariants 1. `execute_prompt` MUST return an `LLMResponse` with a non-empty `content` field on success. 2. `execute_prompt` MUST raise a subclass of `LLMError` on any failure — never a bare exception. 3. `validate_config` MUST be side-effect-free and return `bool` only. 4. `validate_config` returning `False` does not preclude calling `execute_prompt` — it is advisory. 5. Adapters MUST NOT mutate the `config` argument. 6. `execute_prompt` is allowed to be slow (network I/O) but MUST respect `config.timeout_seconds`. ### Failure modes | Condition | Exception | |-----------|-----------| | Missing / invalid API key | `LLMConfigurationError` | | HTTP 4xx (non-429) | `LLMAPIError` (with `.status_code`) | | HTTP 429 | `LLMRateLimitError` | | Request timeout | `LLMTimeoutError` | | CLI subprocess failure | `LLMSubprocessError` (with `.return_code`, `.stderr`) | | Token budget exceeded (WP-0002) | `LLMBudgetExceededError` | ### Compatibility rules - Any code that accepts `LLMAdapter` MUST work with `MockLLMAdapter`. - Adding new optional methods to the ABC is non-breaking (default implementations provided). - Removing or changing the signature of `execute_prompt` or `validate_config` is a **breaking Core change** requiring a major version bump. --- ## RunConfig `llm_connect.models.RunConfig` ### Fields and invariants | Field | Type | Default | Invariant | |-------|------|---------|-----------| | `model_name` | `str` | `"gpt-4"` | Non-empty string; adapters MAY override | | `temperature` | `float` | `0.7` | 0.0 ≤ temperature ≤ 2.0 | | `max_tokens` | `int` | `2000` | > 0 | | `model_params` | `dict` | `{}` | Provider-specific pass-through; no invariants | | `max_depth` | `int` | `3` | ≥ 0 | | `skip_if_exists` | `bool` | `True` | — | | `timeout_seconds` | `int` | `300` | > 0 | | `budget_tracker` | `BudgetTracker \| None` | `None` | Optional; added in WP-0002 | Adapters MUST NOT mutate `RunConfig` fields. --- ## LLMResponse `llm_connect.models.LLMResponse` ### Fields and invariants | Field | Type | Invariant | |-------|------|-----------| | `content` | `str` | Non-empty on success; may be empty only if provider returned empty output | | `model` | `str` | Non-empty; the model actually used (may differ from `RunConfig.model_name`) | | `usage` | `dict` | Keys: `prompt_tokens`, `completion_tokens`, `total_tokens` (all int ≥ 0) | | `finish_reason` | `str` | Provider-reported; `"stop"` is the normal value | | `metadata` | `dict` | Arbitrary; always includes `"provider"` key | --- ## LLMError Hierarchy ``` LLMError ├── LLMConfigurationError bad key / unknown provider ├── LLMAPIError HTTP error (has .status_code, .response_body) │ └── LLMRateLimitError 429 ├── LLMTimeoutError request or subprocess timed out ├── LLMSubprocessError CLI failed (has .return_code, .stderr) └── LLMBudgetExceededError token budget cap exceeded (WP-0002) ``` All exceptions carry optional `cause` (chained exception) and `context` (dict). --- ## Mock adapters `MockLLMAdapter` and `ErrorLLMAdapter` are part of Core — they are test primitives that any consumer may depend on without importing dev extras. `MockLLMAdapter` invariants: - Returns deterministic response without network I/O - Increments `call_count` on each call - Records `last_prompt` and `last_config` - `reset()` clears all counters and recorded state