Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/badges/coverage.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"schemaVersion": 1, "label": "coverage", "message": "59.82%", "color": "red"}
{"schemaVersion": 1, "label": "coverage", "message": "59.78%", "color": "red"}
5 changes: 1 addition & 4 deletions adr/2026-05-28-refactoring-model-forwarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ The new architecture follows the principles of clean architecture: the model for
* **Domain contracts for forwarding:** provider client, provider load balancer, router rate limiter, model tokenizer and environmental impact computer are exposed as domain abstractions. The use case depends on these contracts, not on Redis, HTTP, Ecologit or Tiktoken directly.
* **HTTP client simplified:** `HttpProviderClient` only sends an already formatted request to the selected provider and returns the raw provider response or a model error. It no longer owns endpoint selection, usage computation, metrics or rate limiting.
* **Endpoint adapters extracted:** provider-specific adapters convert OpenGate requests and responses to each provider format. `build_adapter` selects the right adapter from the source endpoint and provider type, while common usage computation stays in the base adapter.
* **Provider gateway replaced by a capabilities probe:** the `ProviderGateway` contract and its `ModelProviderGateway` implementation are replaced by a single `ProviderCapabilitiesProbe` class in the use case layer. Probing a provider for its capabilities (max context length, vector size) is an acquisition concern, not a repository. The class holds no technology of its own: it composes the `ProviderClient` and `ProviderAdapterBuilder` domain contracts and sequences two calls, which makes it an application service shared by `CreateProviderUseCase` and `BootstrapModelsUseCase` rather than an adapter. No abstraction is declared for it, since it is not a boundary the domain needs to invert. It does not appear in the diagram below: the model forwarding use case does not probe capabilities.
* **Redis responsibilities isolated:** Redis implementations handle provider load balancing, provider metrics and router rate limits behind dedicated contracts. The use case decides when those operations happen.
* **Usage and impacts made explicit:** prompt tokens are computed before rate limiting, response usage is computed after provider response formatting, and environmental impacts are delegated to the Ecologit implementation through a domain contract.
* **FastAPI endpoint thinned:** the HTTP endpoint builds the command, calls the use case and maps domain errors to HTTP exceptions. It no longer contains forwarding logic.
Expand All @@ -134,7 +135,6 @@ subgraph DL[**Domain layer**]
provider_adapter_builder[ProviderAdapterBuilder]
provider_adapter[ProviderAdapter]
provider_repository[ProviderRepository]
provider_gateway[ProviderGateway]
provider_load_balancer[ProviderLoadBalancer]
provider_client[ProviderClient]
provider_metrics_logger[ProviderMetricsLogger]
Expand Down Expand Up @@ -187,7 +187,6 @@ use_case --> router_rate_limiter
use_case --> provider_repository
use_case --> provider_adapter_builder
use_case --> provider_adapter
use_case --> provider_gateway
use_case --> provider_load_balancer
use_case --> provider_client
use_case --> provider_metrics_logger
Expand All @@ -198,8 +197,6 @@ use_case --> usage_computer
usage_computer --> model_environmental_impacts_computer
usage_computer --> model_tokenizer
provider_adapter_builder --> http_provider_adapter_builder
provider_client --> http_provider_client
http_provider_adapter_builder --VLLM, Mistral, TEI...<br>Chat completions, OCR, Rerank...--> http_provider_adapter
Expand Down
21 changes: 10 additions & 11 deletions api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from api.domain.provider import (
ProviderAdapterBuilder,
ProviderClient,
ProviderGateway,
ProviderLoadBalancer,
ProviderMetricsLogger,
ProviderRepository,
Expand All @@ -26,7 +25,6 @@
from api.infrastructure.fastapi.context import request_context
from api.infrastructure.http import HttpProviderAdapterBuilder, HttpProviderClient
from api.infrastructure.jwt import JwtKeyEncoder
from api.infrastructure.model import ModelProviderGateway
from api.infrastructure.postgres import (
PostgresAuthenticatedUserQuery,
PostgresKeyRepository,
Expand Down Expand Up @@ -56,6 +54,7 @@
from api.use_cases.health import GetHealthModelsUseCase
from api.use_cases.models import GetModelsUseCase, GetModelUseCase
from api.use_cases.reranks import CreateRerankUseCase
from api.use_cases.services import ProviderCapabilitiesProbe
from api.utils.configuration import configuration
from api.utils.context import global_context

Expand Down Expand Up @@ -122,13 +121,6 @@ def _provider_client() -> ProviderClient:
return HttpProviderClient()


def _provider_gateway(
provider_client: ProviderClient = Depends(_provider_client),
provider_adapter_builder: ProviderAdapterBuilder = Depends(_provider_adapter_builder),
) -> ProviderGateway:
return ModelProviderGateway(provider_client=provider_client, provider_adapter_builder=provider_adapter_builder)


def _provider_load_balancer(redis_client: Redis = Depends(get_redis_client)) -> ProviderLoadBalancer:
return RedisProviderLoadBalancer(redis_client=redis_client)

Expand Down Expand Up @@ -170,6 +162,13 @@ def _provider_repository(session: AsyncSession) -> ProviderRepository:
return PostgresProviderRepository(postgres_session=session)


def _provider_capabilities_probe(
provider_client: ProviderClient = Depends(_provider_client),
provider_adapter_builder: ProviderAdapterBuilder = Depends(_provider_adapter_builder),
) -> ProviderCapabilitiesProbe:
return ProviderCapabilitiesProbe(provider_client=provider_client, provider_adapter_builder=provider_adapter_builder)


# health use cases
def get_health_models_use_case_factory(
postgres_session: AsyncSession = Depends(get_postgres_session),
Expand Down Expand Up @@ -362,12 +361,12 @@ def update_router_use_case_factory(postgres_session: AsyncSession = Depends(get_
# provider use cases
def create_provider_use_case_factory(
postgres_session: AsyncSession = Depends(get_postgres_session),
provider_client: ProviderClient = Depends(_provider_client),
provider_capabilities_probe: ProviderCapabilitiesProbe = Depends(_provider_capabilities_probe),
) -> CreateProviderUseCase:
return CreateProviderUseCase(
router_repository=_router_repository(postgres_session),
provider_repository=_provider_repository(postgres_session),
provider_gateway=_provider_gateway(provider_client=provider_client, provider_adapter_builder=HttpProviderAdapterBuilder()),
provider_capabilities_probe=provider_capabilities_probe,
)


Expand Down
3 changes: 0 additions & 3 deletions api/domain/provider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from api.domain.provider._provideradapter import ProviderAdapter
from api.domain.provider._provideradapterbuilder import ProviderAdapterBuilder
from api.domain.provider._providerclient import ProviderClient, ProviderClientResponse
from api.domain.provider._providergateway import ProviderCapabilities, ProviderGateway
from api.domain.provider._providerloadbalancer import ProviderLoadBalancer
from api.domain.provider._providermetricslogger import ProviderMetricsLogger
from api.domain.provider._providerrepository import ProviderRepository
Expand All @@ -11,8 +10,6 @@
"ProviderAdapterBuilder",
"ProviderClient",
"ProviderClientResponse",
"ProviderCapabilities",
"ProviderGateway",
"ProviderLoadBalancer",
"ProviderMetricsLogger",
"ProviderRepository",
Expand Down
27 changes: 0 additions & 27 deletions api/domain/provider/_providergateway.py

This file was deleted.

5 changes: 5 additions & 0 deletions api/domain/provider/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ def is_compatible_with(self, router: Router) -> bool:
return self.type.is_compatible_with(router.type)


class ProviderCapabilities(BaseModel):
max_context_length: int | None
vector_size: int | None = None


class ProviderOriginalRequest(BaseModel):
endpoint: Annotated[EndpointRoute, Field(description="The source endpoint (at the user side) of the request.")]
body: Annotated[CreateEmbeddingsBody | CreateRerankBody | None, Field(default=None, description="The JSON body to use for the request.")]
Expand Down
7 changes: 6 additions & 1 deletion api/infrastructure/fastapi/endpoints/admin/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
update_provider_use_case_factory,
)
from api.domain import SortOrder
from api.domain.model.errors import InconsistentModelMaxContextLengthError, InconsistentModelVectorSizeError
from api.domain.model.errors import InconsistentModelMaxContextLengthError, InconsistentModelVectorSizeError, ModelNotFoundError
from api.domain.provider.entities import ProviderSortField
from api.domain.provider.errors import InvalidProviderTypeError, ProviderAlreadyExistsError, ProviderNotFoundError, ProviderNotReachableError
from api.domain.router.errors import RouterNotFoundError
Expand All @@ -25,6 +25,7 @@
InconsistentModelVectorSizeHTTPException,
InternalServerHTTPException,
InvalidProviderTypeHTTPException,
ModelNotFoundHTTPException,
NotAdminUserHTTPException,
ProviderAlreadyExistsHTTPException,
ProviderNotFoundHTTPException,
Expand Down Expand Up @@ -70,6 +71,7 @@
InconsistentModelVectorSizeHTTPException,
InvalidProviderTypeHTTPException,
ProviderNotReachableHTTPException,
ModelNotFoundHTTPException,
ProviderAlreadyExistsHTTPException,
RouterNotFoundHTTPException,
NotAdminUserHTTPException,
Expand Down Expand Up @@ -127,6 +129,9 @@ async def create_provider(
case ProviderNotReachableError() as error:
raise ProviderNotReachableHTTPException(name=error.model_name, status_code=error.status_code, detail=error.detail)

case ModelNotFoundError(name=model_name):
raise ModelNotFoundHTTPException(name=model_name)

case ProviderAlreadyExistsError(model_name=model_name, url=url, router_id=router_id):
raise ProviderAlreadyExistsHTTPException(model_name=model_name, url=url, router_id=router_id)

Expand Down
3 changes: 0 additions & 3 deletions api/infrastructure/model/__init__.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from api.dependencies import create_provider_use_case_factory
from api.domain.model.entities import ModelType as RouterType
from api.domain.model.errors import InconsistentModelMaxContextLengthError, InconsistentModelVectorSizeError
from api.domain.model.errors import InconsistentModelMaxContextLengthError, InconsistentModelVectorSizeError, ModelNotFoundError
from api.domain.provider.entities import ProviderType
from api.domain.provider.errors import InvalidProviderTypeError, ProviderAlreadyExistsError, ProviderNotReachableError
from api.domain.router.errors import RouterNotFoundError
Expand Down Expand Up @@ -84,6 +84,11 @@ async def test_happy_path(self, client: AsyncClient, db_session):
424,
"Model provider my-model not reachable (500): error_detail",
),
(
ModelNotFoundError(name="my-model"),
404,
"Model my-model not found.",
),
(
ProviderAlreadyExistsError(model_name="my-model", url=DEFAULT_PROVIDER_URL, router_id=1),
409,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
import respx

from api.domain.model.entities import ModelType as RouterType
from api.domain.provider import ProviderCapabilities
from api.domain.provider.entities import ProviderType
from api.domain.provider.entities import ProviderCapabilities, ProviderType
from api.infrastructure.http import HttpProviderAdapterBuilder, HttpProviderClient
from api.infrastructure.model import ModelProviderGateway
from api.tests.integration.factories.tei import TeiEmbeddingsResponseFactory, TeiModelsResponseFactory
from api.tests.integration.factories.vllm import VllmModelsResponseFactory
from api.use_cases.services import ProviderCapabilitiesProbe

DEFAULT_PROVIDER_URL = "http://my-test-provider/"
DEFAULT_MODEL_ID = "test/my-model"
Expand All @@ -32,22 +31,22 @@ def _mock_embeddings_response(respx_mock, body: dict, status_code: int) -> None:


@pytest.fixture
def gateway() -> ModelProviderGateway:
return ModelProviderGateway(provider_client=HttpProviderClient(), provider_adapter_builder=HttpProviderAdapterBuilder())
def probe() -> ProviderCapabilitiesProbe:
return ProviderCapabilitiesProbe(provider_client=HttpProviderClient(), provider_adapter_builder=HttpProviderAdapterBuilder())


@pytest.mark.asyncio(loop_scope="session")
class TestModelProviderGateway:
class TestProviderCapabilitiesProbe:
@respx.mock
async def test_get_capabilities_of_non_embeddings_providers(self, gateway: ModelProviderGateway):
async def test_get_capabilities_of_non_embeddings_providers(self, probe: ProviderCapabilitiesProbe):
_mock_models_response(
respx_mock=respx,
provider_type=ProviderType.VLLM,
body=VllmModelsResponseFactory(model_id=DEFAULT_MODEL_ID, max_context_length=DEFAULT_MAX_CONTEXT_LENGTH),
status_code=VllmModelsResponseFactory._status_code,
)

result = await gateway.get_capabilities(
result = await probe.get_capabilities(
router_type=RouterType.TEXT_GENERATION,
provider_type=ProviderType.VLLM,
url=DEFAULT_PROVIDER_URL,
Expand All @@ -61,7 +60,7 @@ async def test_get_capabilities_of_non_embeddings_providers(self, gateway: Model
@respx.mock
async def test_get_capabilities_of_embeddings_providers(
self,
gateway: ModelProviderGateway,
probe: ProviderCapabilitiesProbe,
):
_mock_models_response(
respx_mock=respx,
Expand All @@ -75,7 +74,7 @@ async def test_get_capabilities_of_embeddings_providers(
status_code=TeiEmbeddingsResponseFactory._status_code,
)

result = await gateway.get_capabilities(
result = await probe.get_capabilities(
router_type=RouterType.TEXT_EMBEDDINGS_INFERENCE,
provider_type=ProviderType.TEI,
url=DEFAULT_PROVIDER_URL,
Expand Down
Loading