Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions packages/iris-engine/src/iris_engine/contracts/llm_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""LLMProvider Protocol, request/response types, and typed errors.

No adapter imports. This module must remain importable without any LLM
dependency installed.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol

from pydantic import BaseModel

from iris_engine.contracts.ocr_engine import TenantContext

__all__ = [
"VALID_ADAPTER_IDS",
"LLMError",
"LLMUnavailable",
"LLMRateLimited",
"LLMAuthenticationFailed",
"LLMSchemaViolation",
"LLMContextWindowExceeded",
"LLMContentFiltered",
"LLMInvalidRequest",
"LLMUsage",
"LLMResponse",
"LLMRequest",
"LLMProvider",
"TenantContext",
]

VALID_ADAPTER_IDS: frozenset[str] = frozenset({"azure-openai", "openai", "anthropic", "local"})


# ── error hierarchy ───────────────────────────────────────────────────────────


class LLMError(Exception):
"""Base for all typed LLM errors."""


class LLMUnavailable(LLMError):
"""Network failure, 5xx, or timeout."""


class LLMRateLimited(LLMError):
"""429 or quota exhausted."""


class LLMAuthenticationFailed(LLMError):
"""401 or 403; invalid or expired credentials."""


class LLMSchemaViolation(LLMError):
"""Structured output did not match the requested schema."""


class LLMContextWindowExceeded(LLMError):
"""Prompt + max_output_tokens exceeds the model context limit."""


class LLMContentFiltered(LLMError):
"""Provider content policy blocked the request."""


class LLMInvalidRequest(LLMError):
"""400 from the provider; request is malformed."""


# ── data types ────────────────────────────────────────────────────────────────


@dataclass(frozen=True)
class LLMUsage:
input_tokens: int
output_tokens: int
total_tokens: int # must equal input_tokens + output_tokens


@dataclass(frozen=True)
class LLMResponse:
text: str
structured: BaseModel | None # populated when schema was supplied in the request
model: str # model identifier the adapter used
adapter_id: str # one of VALID_ADAPTER_IDS or "in-memory" for the stub
usage: LLMUsage
latency_ms: int


@dataclass(frozen=True)
class LLMRequest:
prompt: str
system: str | None = None
schema: type[BaseModel] | None = None
temperature: float = 0.0
max_output_tokens: int | None = None
stop: list[str] | None = None
model_hint: str | None = None # "extraction" | "classification" | "summary" | "chat"


# ── Protocol ──────────────────────────────────────────────────────────────────


class LLMProvider(Protocol):
id: str
version: str

async def complete(
self,
ctx: TenantContext,
request: LLMRequest,
) -> LLMResponse:
"""Call the LLM and return a structured response.

Raises:
LLMUnavailable: network or service failure.
LLMRateLimited: quota exceeded.
LLMAuthenticationFailed: invalid or expired credentials.
LLMSchemaViolation: structured output did not match the schema.
LLMContextWindowExceeded: prompt exceeds model context limit.
LLMContentFiltered: provider content policy blocked the request.
LLMInvalidRequest: malformed request (400).
"""
...
2 changes: 1 addition & 1 deletion tasks/004-llm-adapter-set/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Sprint 1: Protocol + selector

- [ ] **T040** `[US2] [US8] [size: S] [owner: AuroPro]` Add `iris_engine/contracts/llm_provider.py` with the Protocol, `LLMRequest`, `LLMResponse`, `LLMUsage`, and the error types from the contract document.
- [x] **T040** `[US2] [US8] [size: S] [owner: AuroPro]` Add `iris_engine/contracts/llm_provider.py` with the Protocol, `LLMRequest`, `LLMResponse`, `LLMUsage`, and the error types from the contract document.
**Acceptance**: mypy strict passes; the module imports with zero adapter dependencies.

- [ ] **T041** `[P] [US1] [size: M] [owner: AuroPro]` Implement `iris_engine/llm/selector.py`. Reads `ProductConfig.adapters.llm`, returns the configured `LLMProvider`. Optional fallback path.
Expand Down