-
Notifications
You must be signed in to change notification settings - Fork 1
feat: design unified provider interface #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
76f3b4c
5af5a7f
fb2f922
f9ae175
520e14e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -48,6 +48,7 @@ src/ai_company/ | |||||
| ## Code Conventions | ||||||
|
|
||||||
| - **No `from __future__ import annotations`** — Python 3.14 has PEP 649 | ||||||
| - **PEP 758 except syntax**: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14 | ||||||
|
||||||
| - **PEP 758 except syntax**: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14 | |
| - **Exception syntax**: for multiple exceptions use `except (A, B):` (tuple) — Python 3 only supports the tuple form |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| """Unified provider interface for LLM completion. | ||
|
|
||
| Exports protocols, base classes, domain models, enums, and errors | ||
| for the provider layer. | ||
| """ | ||
|
|
||
| from .base import BaseCompletionProvider | ||
| from .capabilities import ModelCapabilities | ||
| from .enums import FinishReason, MessageRole, StreamEventType | ||
| from .errors import ( | ||
| AuthenticationError, | ||
| ContentFilterError, | ||
| InvalidRequestError, | ||
| ModelNotFoundError, | ||
| ProviderConnectionError, | ||
| ProviderError, | ||
| ProviderInternalError, | ||
| ProviderTimeoutError, | ||
| RateLimitError, | ||
| ) | ||
| from .models import ( | ||
| ChatMessage, | ||
| CompletionConfig, | ||
| CompletionResponse, | ||
| StreamChunk, | ||
| TokenUsage, | ||
| ToolCall, | ||
| ToolDefinition, | ||
| ToolResult, | ||
| ) | ||
| from .protocol import CompletionProvider | ||
|
|
||
| __all__ = [ | ||
| "AuthenticationError", | ||
| "BaseCompletionProvider", | ||
| "ChatMessage", | ||
| "CompletionConfig", | ||
| "CompletionProvider", | ||
| "CompletionResponse", | ||
| "ContentFilterError", | ||
| "FinishReason", | ||
| "InvalidRequestError", | ||
| "MessageRole", | ||
| "ModelCapabilities", | ||
| "ModelNotFoundError", | ||
| "ProviderConnectionError", | ||
| "ProviderError", | ||
| "ProviderInternalError", | ||
| "ProviderTimeoutError", | ||
| "RateLimitError", | ||
| "StreamChunk", | ||
| "StreamEventType", | ||
| "TokenUsage", | ||
| "ToolCall", | ||
| "ToolDefinition", | ||
| "ToolResult", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,271 @@ | ||||||||||||||||||||||||||||||||||||
| """Abstract base class for completion providers. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Concrete adapters subclass ``BaseCompletionProvider`` and implement | ||||||||||||||||||||||||||||||||||||
| the ``_do_*`` hooks. The base class handles input validation and | ||||||||||||||||||||||||||||||||||||
| provides a cost-computation helper. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||||||
| from abc import ABC, abstractmethod | ||||||||||||||||||||||||||||||||||||
| from collections.abc import AsyncIterator # noqa: TC003 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| from .capabilities import ModelCapabilities # noqa: TC001 | ||||||||||||||||||||||||||||||||||||
| from .errors import InvalidRequestError | ||||||||||||||||||||||||||||||||||||
| from .models import ( | ||||||||||||||||||||||||||||||||||||
| ChatMessage, | ||||||||||||||||||||||||||||||||||||
| CompletionConfig, | ||||||||||||||||||||||||||||||||||||
| CompletionResponse, | ||||||||||||||||||||||||||||||||||||
| StreamChunk, | ||||||||||||||||||||||||||||||||||||
| TokenUsage, | ||||||||||||||||||||||||||||||||||||
| ToolDefinition, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| _COST_ROUNDING_PRECISION: int = 10 | ||||||||||||||||||||||||||||||||||||
| """Decimal places for cost rounding to avoid floating-point dust.""" | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| class BaseCompletionProvider(ABC): | ||||||||||||||||||||||||||||||||||||
| """Shared base for all completion provider adapters. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Subclasses implement three hooks: | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| * ``_do_complete`` — raw non-streaming call | ||||||||||||||||||||||||||||||||||||
| * ``_do_stream`` — raw streaming call | ||||||||||||||||||||||||||||||||||||
| * ``_do_get_model_capabilities`` — capability lookup | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| The public methods validate inputs before delegating to hooks. | ||||||||||||||||||||||||||||||||||||
| A static ``compute_cost`` helper is available for subclasses to | ||||||||||||||||||||||||||||||||||||
| build ``TokenUsage`` records from raw token counts. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # -- Public API --------------------------------------------------- | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| async def complete( | ||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||
| messages: list[ChatMessage], | ||||||||||||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||
| tools: list[ToolDefinition] | None = None, | ||||||||||||||||||||||||||||||||||||
| config: CompletionConfig | None = None, | ||||||||||||||||||||||||||||||||||||
| ) -> CompletionResponse: | ||||||||||||||||||||||||||||||||||||
| """Validate inputs, delegate to ``_do_complete``. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||
| messages: Conversation history. | ||||||||||||||||||||||||||||||||||||
| model: Model identifier to use. | ||||||||||||||||||||||||||||||||||||
| tools: Available tools for function calling. | ||||||||||||||||||||||||||||||||||||
| config: Optional completion parameters. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||
| The completion response returned by the subclass | ||||||||||||||||||||||||||||||||||||
| ``_do_complete`` hook, unmodified. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||||||||
| InvalidRequestError: If messages are empty or model is blank. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| self._validate_messages(messages) | ||||||||||||||||||||||||||||||||||||
| self._validate_model(model) | ||||||||||||||||||||||||||||||||||||
| return await self._do_complete( | ||||||||||||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||||||||||||||
| tools=tools, | ||||||||||||||||||||||||||||||||||||
| config=config, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| async def stream( | ||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||
| messages: list[ChatMessage], | ||||||||||||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||
| tools: list[ToolDefinition] | None = None, | ||||||||||||||||||||||||||||||||||||
| config: CompletionConfig | None = None, | ||||||||||||||||||||||||||||||||||||
| ) -> AsyncIterator[StreamChunk]: | ||||||||||||||||||||||||||||||||||||
| """Validate inputs, delegate to ``_do_stream``. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||
| messages: Conversation history. | ||||||||||||||||||||||||||||||||||||
| model: Model identifier to use. | ||||||||||||||||||||||||||||||||||||
| tools: Available tools for function calling. | ||||||||||||||||||||||||||||||||||||
| config: Optional completion parameters. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||
| Async iterator of stream chunks returned by the subclass | ||||||||||||||||||||||||||||||||||||
| ``_do_stream`` hook, unmodified. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||||||||
| InvalidRequestError: If messages are empty or model is blank. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| self._validate_messages(messages) | ||||||||||||||||||||||||||||||||||||
| self._validate_model(model) | ||||||||||||||||||||||||||||||||||||
| return await self._do_stream( | ||||||||||||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||||||||||||||
| tools=tools, | ||||||||||||||||||||||||||||||||||||
| config=config, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| async def get_model_capabilities(self, model: str) -> ModelCapabilities: | ||||||||||||||||||||||||||||||||||||
| """Validate model identifier, delegate to ``_do_get_model_capabilities``. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||
| model: Model identifier. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||
| Static capability and cost information. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||||||||
| InvalidRequestError: If model is blank. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| self._validate_model(model) | ||||||||||||||||||||||||||||||||||||
| return await self._do_get_model_capabilities(model) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # -- Hooks (subclasses implement) --------------------------------- | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @abstractmethod | ||||||||||||||||||||||||||||||||||||
| async def _do_complete( | ||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||
| messages: list[ChatMessage], | ||||||||||||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||
| tools: list[ToolDefinition] | None = None, | ||||||||||||||||||||||||||||||||||||
| config: CompletionConfig | None = None, | ||||||||||||||||||||||||||||||||||||
| ) -> CompletionResponse: | ||||||||||||||||||||||||||||||||||||
| """Provider-specific non-streaming completion. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Subclasses **must** catch all provider-specific exceptions and | ||||||||||||||||||||||||||||||||||||
| re-raise them as appropriate ``ProviderError`` subclasses. | ||||||||||||||||||||||||||||||||||||
| Exceptions that escape without wrapping will bypass the error | ||||||||||||||||||||||||||||||||||||
| hierarchy. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||||||||
| ProviderError: All errors must use the provider error hierarchy. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| ... | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @abstractmethod | ||||||||||||||||||||||||||||||||||||
| async def _do_stream( | ||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||
| messages: list[ChatMessage], | ||||||||||||||||||||||||||||||||||||
| model: str, | ||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||
| tools: list[ToolDefinition] | None = None, | ||||||||||||||||||||||||||||||||||||
| config: CompletionConfig | None = None, | ||||||||||||||||||||||||||||||||||||
| ) -> AsyncIterator[StreamChunk]: | ||||||||||||||||||||||||||||||||||||
| r"""Provider-specific streaming completion. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Implementations must *return* an ``AsyncIterator`` (not ``yield`` | ||||||||||||||||||||||||||||||||||||
| directly), since the caller ``await``\s this coroutine to obtain | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| directly), since the caller ``await``\s this coroutine to obtain | |
| directly), since the caller awaits this coroutine to obtain |
Copilot
AI
Mar 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compute_cost() rejects non-finite rates (NaN/inf) in addition to negatives, but the docstring says it only raises InvalidRequestError when a parameter is negative. Please update the docstring to reflect the actual validation behavior (non-negative and finite).
| cost_per_1k_input: Cost per 1 000 input tokens in USD (>= 0). | |
| cost_per_1k_output: Cost per 1 000 output tokens in USD (>= 0). | |
| Returns: | |
| Populated ``TokenUsage`` with computed cost. | |
| Raises: | |
| InvalidRequestError: If any parameter is negative. | |
| cost_per_1k_input: Cost per 1 000 input tokens in USD (finite and >= 0). | |
| cost_per_1k_output: Cost per 1 000 output tokens in USD (finite and >= 0). | |
| Returns: | |
| Populated ``TokenUsage`` with computed cost. | |
| Raises: | |
| InvalidRequestError: If any parameter is negative or non-finite | |
| (for example, NaN or infinity). |
Copilot
AI
Mar 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compute_cost() hard-codes the rounding precision (round(cost, 10)), which makes the intent and consistency across the codebase harder to manage. There is already a shared BUDGET_ROUNDING_PRECISION = 10 constant used elsewhere to avoid float artifacts; consider reusing a shared constant (or introducing a provider-specific one) instead of a magic number here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documented syntax
except A, B:for multiple exceptions is from Python 2 and is invalid in Python 3. The correct syntax in Python 3 is to use a tuple:except (A, B):.Additionally, I couldn't find a reference to PEP 758. This might be a typo.
To prevent confusion for developers, it would be good to update this convention.