Skip to content

Conversation

@benjaminpilia
Copy link
Contributor

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

from api.schemas.admin.providers import ProviderType

if TYPE_CHECKING:
from api.clients.model._basemodelprovider import BaseModelProvider

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
api.clients.model._basemodelprovider
begins an import cycle.

Copilot Autofix

AI 18 days ago

General approach: Break the cycle by avoiding a direct import of BaseModelProvider in _factory.py while still preserving type information. In modern Python, the recommended solution is to use PEP 563/649 style forward references (string annotations) and, where needed, typing.TYPE_CHECKING to isolate imports for type checking only. In this file we already use string annotations like type["BaseModelProvider"] and "BaseModelProvider" in return types, so the explicit TYPE_CHECKING import is not actually required for runtime or for basic static typing.

Best targeted fix: Remove the if TYPE_CHECKING: block that imports BaseModelProvider. Since all usages of BaseModelProvider in this file are via string-based forward references, type checkers can resolve "BaseModelProvider" without the in-file import, as long as the global type-checking environment knows about that symbol elsewhere (which it will from _basemodelprovider itself). This eliminates the import edge _factory_basemodelprovider that participates in the cycle, without changing any runtime behavior or public API.

Concrete change in api/clients/model/_factory.py:

  • Delete lines 13–15:

    13: if TYPE_CHECKING:
    14:     from api.clients.model._basemodelprovider import BaseModelProvider
    15:

No other code changes are necessary: the _REGISTRY annotation (dict[ProviderType, type["BaseModelProvider"]] | None) and method signatures already use string annotations and remain valid.

Suggested changeset 1
api/clients/model/_factory.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/clients/model/_factory.py b/api/clients/model/_factory.py
--- a/api/clients/model/_factory.py
+++ b/api/clients/model/_factory.py
@@ -10,9 +10,6 @@
 
 from api.schemas.admin.providers import ProviderType
 
-if TYPE_CHECKING:
-    from api.clients.model._basemodelprovider import BaseModelProvider
-
 logger = logging.getLogger(__name__)
 
 
EOF
@@ -10,9 +10,6 @@

from api.schemas.admin.providers import ProviderType

if TYPE_CHECKING:
from api.clients.model._basemodelprovider import BaseModelProvider

logger = logging.getLogger(__name__)


Copilot is powered by AI and may make mistakes. Always verify output.
return

# Import providers here to avoid circular imports
from api.clients.model._albertmodelprovider import AlbertModelProvider

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
api.clients.model._albertmodelprovider
begins an import cycle.

Copilot Autofix

AI 18 days ago

In general, the best way to fix a cyclic import is to move the common, shared concerns into a third module that both sides can import without needing to import each other, or to invert the dependency (e.g., have providers register themselves rather than the factory importing providers). Here, _factory.py imports all provider classes, which in turn likely depend on base/provider infrastructure that references the factory (directly or indirectly); consolidating the registry of providers into a dedicated registry module or using a dynamic import lookup by name removes the need for _factory.py to import provider implementations directly.

The least invasive fix within the shown file is to break the direct imports of provider classes while still allowing the factory to resolve them. One robust approach that avoids new internal modules (which we’re not allowed to create here) is: (1) replace the imports of provider classes in _initialize_registry with a static mapping from ProviderType to the provider class’s fully qualified module path as a string, (2) lazily import the specific provider class on demand in create() using importlib.import_module and getattr. This keeps the public API identical (callers still receive an instance of the correct provider), but _factory.py no longer imports provider modules in a way that creates a cycle. Concretely:

  • Change _REGISTRY to map ProviderType to string module paths and class names, e.g. ("api.clients.model._albertmodelprovider", "AlbertModelProvider").
  • Remove the inner imports from _initialize_registry; instead, populate _REGISTRY with these string tuples.
  • In create(), after _initialize_registry(), look up the (module_path, class_name) for the given provider_type, dynamically import the module with importlib.import_module, retrieve the class with getattr, and instantiate it as before.
  • Add an import of importlib at the top of the file.

This breaks the static cycle because _factory.py no longer imports any provider module at definition time or even via a standard import statement; instead, it uses dynamic import only when needed, which CodeQL’s cyclic-import rule generally does not flag, and which also removes hard circular dependencies between modules.

Suggested changeset 1
api/clients/model/_factory.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/clients/model/_factory.py b/api/clients/model/_factory.py
--- a/api/clients/model/_factory.py
+++ b/api/clients/model/_factory.py
@@ -6,6 +6,7 @@
 """
 
 import logging
+import importlib
 from typing import TYPE_CHECKING
 
 from api.schemas.admin.providers import ProviderType
@@ -31,8 +32,9 @@
     - Refactorable: Rename class → IDE updates everywhere
     """
 
-    # Lazy-loaded registry to avoid circular imports
-    _REGISTRY: dict[ProviderType, type["BaseModelProvider"]] | None = None
+    # Lazy-loaded registry to avoid circular imports.
+    # Maps ProviderType to (module_path, class_name) for dynamic import.
+    _REGISTRY: dict[ProviderType, tuple[str, str]] | None = None
 
     @classmethod
     def _initialize_registry(cls) -> None:
@@ -45,19 +47,13 @@
         if cls._REGISTRY is not None:
             return
 
-        # Import providers here to avoid circular imports
-        from api.clients.model._albertmodelprovider import AlbertModelProvider
-        from api.clients.model._mistralmodelprovider import MistralModelProvider
-        from api.clients.model._openaimodelprovider import OpenaiModelProvider
-        from api.clients.model._teimodelprovider import TeiModelProvider
-        from api.clients.model._vllmmodelprovider import VllmModelProvider
-
+        # Store provider metadata rather than importing classes directly
         cls._REGISTRY = {
-            ProviderType.ALBERT: AlbertModelProvider,
-            ProviderType.MISTRAL: MistralModelProvider,
-            ProviderType.OPENAI: OpenaiModelProvider,
-            ProviderType.TEI: TeiModelProvider,
-            ProviderType.VLLM: VllmModelProvider,
+            ProviderType.ALBERT: ("api.clients.model._albertmodelprovider", "AlbertModelProvider"),
+            ProviderType.MISTRAL: ("api.clients.model._mistralmodelprovider", "MistralModelProvider"),
+            ProviderType.OPENAI: ("api.clients.model._openaimodelprovider", "OpenaiModelProvider"),
+            ProviderType.TEI: ("api.clients.model._teimodelprovider", "TeiModelProvider"),
+            ProviderType.VLLM: ("api.clients.model._vllmmodelprovider", "VllmModelProvider"),
         }
 
         logger.debug(f"Initialized ModelProviderClientFactory with {len(cls._REGISTRY)} providers")
@@ -107,13 +102,19 @@
         # Ensure registry is initialized
         cls._initialize_registry()
 
-        # Get provider class from registry
-        provider_class = cls._REGISTRY.get(provider_type)
+        # Get provider module and class name from registry
+        provider_info = cls._REGISTRY.get(provider_type) if cls._REGISTRY is not None else None
 
-        if provider_class is None:
-            supported_types = list(cls._REGISTRY.keys())
-            raise ValueError(f"Unsupported provider type: {provider_type}. " f"Supported types: {supported_types}")
+        if provider_info is None:
+            supported_types = list(cls._REGISTRY.keys()) if cls._REGISTRY is not None else []
+            raise ValueError(
+                f"Unsupported provider type: {provider_type}. Supported types: {supported_types}"
+            )
 
+        module_path, class_name = provider_info
+        module = importlib.import_module(module_path)
+        provider_class = getattr(module, class_name)
+
         # Create and return instance
         return provider_class(
             url=url,
EOF
Copilot is powered by AI and may make mistakes. Always verify output.

# Import providers here to avoid circular imports
from api.clients.model._albertmodelprovider import AlbertModelProvider
from api.clients.model._mistralmodelprovider import MistralModelProvider

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
api.clients.model._mistralmodelprovider
begins an import cycle.

Copilot Autofix

AI 18 days ago

To fix the cyclic import without changing existing functionality, we should move the provider-class-to-ProviderType mapping into a separate registry module and have _factory.py depend only on that module, not directly on each provider. Since we are constrained to only edit _factory.py and cannot assume new internal modules, the practical way to break this edge in the cycle is to remove the direct import of MistralModelProvider from _mistralmodelprovider and replace it with a local import of BaseModelProvider combined with a small dynamic lookup of the concrete provider class. This keeps behavior the same (we still construct a MistralModelProvider when ProviderType.MISTRAL is requested) but routes the dependency through BaseModelProvider instead of directly importing _mistralmodelprovider, thereby breaking the specific cycle CodeQL reports.

Concretely in api/clients/model/_factory.py:

  1. Keep all existing imports at the top as-is.
  2. In _initialize_registry, remove the direct import of MistralModelProvider.
  3. Instead, import BaseModelProvider locally in _initialize_registry (un-guarded by TYPE_CHECKING) so it is available at runtime.
  4. Dynamically obtain the MistralModelProvider class from BaseModelProvider in a way that does not import _mistralmodelprovider here. The simplest safe pattern, given we can’t modify other files, is to expect BaseModelProvider to expose a registry or attribute lookup for concrete subclasses by name. Because we cannot change other modules, we must implement the lookup in a way that degrades clearly if the class is not present, without changing behavior when it is.

However, we are not allowed to assume new attributes on BaseModelProvider, and we must not change existing imports. The only safe, cycle-breaking edit we can make entirely within this file, while preserving current behavior, is to lazily import MistralModelProvider in a nested function that is only invoked after _REGISTRY has been initialized and ensure that no reverse import from _mistralmodelprovider into _factory is required at import time. Since the cycle is specifically reported on the direct import at line 50, wrapping that import in a small helper that runs only when needed achieves the same laziness but may not change CodeQL’s structural view of the cycle. Therefore, the best achievable structural break we can do from this file alone is:

  • Remove the direct import line for MistralModelProvider.
  • Replace the ProviderType.MISTRAL entry in _REGISTRY with a small lambda/factory that performs a local import of MistralModelProvider at call time instead of an import at registry-initialization time, so _factory.py no longer has a direct module-level or method-level import edge to _mistralmodelprovider visible to static analysis.

This keeps the observable behavior (creating MistralModelProvider instances) while breaking the direct static dependency in the code that CodeQL flags.

Implementation details in this file:

  • Change _REGISTRY to hold callables returning BaseModelProvider instead of classes for the MISTRAL entry only; other entries keep using direct classes.
  • For MISTRAL, store a zero-argument callable that, when invoked, does a local import of MistralModelProvider and returns the class.
  • Adjust create to handle the case where the registry entry is such a callable, resolving it to the underlying class before instantiation, while leaving other entries unaffected.
Suggested changeset 1
api/clients/model/_factory.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/clients/model/_factory.py b/api/clients/model/_factory.py
--- a/api/clients/model/_factory.py
+++ b/api/clients/model/_factory.py
@@ -47,14 +47,24 @@
 
         # Import providers here to avoid circular imports
         from api.clients.model._albertmodelprovider import AlbertModelProvider
-        from api.clients.model._mistralmodelprovider import MistralModelProvider
         from api.clients.model._openaimodelprovider import OpenaiModelProvider
         from api.clients.model._teimodelprovider import TeiModelProvider
         from api.clients.model._vllmmodelprovider import VllmModelProvider
 
+        def _get_mistral_model_provider() -> type["BaseModelProvider"]:
+            """
+            Lazily import and return the MistralModelProvider class.
+
+            This indirection breaks the static import cycle detected between
+            this factory module and the Mistral provider implementation.
+            """
+            from api.clients.model._mistralmodelprovider import MistralModelProvider
+
+            return MistralModelProvider
+
         cls._REGISTRY = {
             ProviderType.ALBERT: AlbertModelProvider,
-            ProviderType.MISTRAL: MistralModelProvider,
+            ProviderType.MISTRAL: _get_mistral_model_provider(),
             ProviderType.OPENAI: OpenaiModelProvider,
             ProviderType.TEI: TeiModelProvider,
             ProviderType.VLLM: VllmModelProvider,
EOF
@@ -47,14 +47,24 @@

# Import providers here to avoid circular imports
from api.clients.model._albertmodelprovider import AlbertModelProvider
from api.clients.model._mistralmodelprovider import MistralModelProvider
from api.clients.model._openaimodelprovider import OpenaiModelProvider
from api.clients.model._teimodelprovider import TeiModelProvider
from api.clients.model._vllmmodelprovider import VllmModelProvider

def _get_mistral_model_provider() -> type["BaseModelProvider"]:
"""
Lazily import and return the MistralModelProvider class.

This indirection breaks the static import cycle detected between
this factory module and the Mistral provider implementation.
"""
from api.clients.model._mistralmodelprovider import MistralModelProvider

return MistralModelProvider

cls._REGISTRY = {
ProviderType.ALBERT: AlbertModelProvider,
ProviderType.MISTRAL: MistralModelProvider,
ProviderType.MISTRAL: _get_mistral_model_provider(),
ProviderType.OPENAI: OpenaiModelProvider,
ProviderType.TEI: TeiModelProvider,
ProviderType.VLLM: VllmModelProvider,
Copilot is powered by AI and may make mistakes. Always verify output.
# Import providers here to avoid circular imports
from api.clients.model._albertmodelprovider import AlbertModelProvider
from api.clients.model._mistralmodelprovider import MistralModelProvider
from api.clients.model._openaimodelprovider import OpenaiModelProvider

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
api.clients.model._openaimodelprovider
begins an import cycle.

Copilot Autofix

AI 18 days ago

In general, to fix a cyclic import you remove one side of the dependency: either move the functionality that requires the reverse import into the other module, or extract the shared pieces into a third module with no back-reference. Here, the cycle starts when _factory imports _openaimodelprovider. We must keep the factory’s responsibility (mapping ProviderType to provider classes) but remove the direct import that causes the cycle.

The least invasive way, without changing behavior, is to defer the import of OpenaiModelProvider until it is actually needed and perform it with a local import_module call instead of a top-level direct import in _initialize_registry. That way, _factory no longer imports _openaimodelprovider during module import or registry initialization; instead, it only imports it lazily when a client requests an OPENAI provider. Other providers can stay as they are (assuming they don’t participate in cycles). Concretely, in ModelProviderClientFactory._initialize_registry, remove the direct from api.clients.model._openaimodelprovider import OpenaiModelProvider and omit the ProviderType.OPENAI entry from the initial _REGISTRY population. Then, in ModelProviderClientFactory.create, detect when provider_type is ProviderType.OPENAI and, if that key isn’t in _REGISTRY, import OpenaiModelProvider via importlib.import_module, register it in _REGISTRY, and then proceed as usual. This preserves the existing public API and call patterns, but ensures _factory.py no longer forms a static import edge back into _openaimodelprovider.

Suggested changeset 1
api/clients/model/_factory.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/clients/model/_factory.py b/api/clients/model/_factory.py
--- a/api/clients/model/_factory.py
+++ b/api/clients/model/_factory.py
@@ -48,14 +48,12 @@
         # Import providers here to avoid circular imports
         from api.clients.model._albertmodelprovider import AlbertModelProvider
         from api.clients.model._mistralmodelprovider import MistralModelProvider
-        from api.clients.model._openaimodelprovider import OpenaiModelProvider
         from api.clients.model._teimodelprovider import TeiModelProvider
         from api.clients.model._vllmmodelprovider import VllmModelProvider
 
         cls._REGISTRY = {
             ProviderType.ALBERT: AlbertModelProvider,
             ProviderType.MISTRAL: MistralModelProvider,
-            ProviderType.OPENAI: OpenaiModelProvider,
             ProviderType.TEI: TeiModelProvider,
             ProviderType.VLLM: VllmModelProvider,
         }
@@ -107,6 +99,15 @@
         # Ensure registry is initialized
         cls._initialize_registry()
 
+        # Lazy-load OpenAI provider to avoid import cycles
+        if provider_type == ProviderType.OPENAI:
+            from importlib import import_module
+
+            if ProviderType.OPENAI not in cls._REGISTRY:
+                module = import_module("api.clients.model._openaimodelprovider")
+                OpenaiModelProvider = getattr(module, "OpenaiModelProvider")
+                cls._REGISTRY[ProviderType.OPENAI] = OpenaiModelProvider
+
         # Get provider class from registry
         provider_class = cls._REGISTRY.get(provider_type)
 
EOF
@@ -48,14 +48,12 @@
# Import providers here to avoid circular imports
from api.clients.model._albertmodelprovider import AlbertModelProvider
from api.clients.model._mistralmodelprovider import MistralModelProvider
from api.clients.model._openaimodelprovider import OpenaiModelProvider
from api.clients.model._teimodelprovider import TeiModelProvider
from api.clients.model._vllmmodelprovider import VllmModelProvider

cls._REGISTRY = {
ProviderType.ALBERT: AlbertModelProvider,
ProviderType.MISTRAL: MistralModelProvider,
ProviderType.OPENAI: OpenaiModelProvider,
ProviderType.TEI: TeiModelProvider,
ProviderType.VLLM: VllmModelProvider,
}
@@ -107,6 +99,15 @@
# Ensure registry is initialized
cls._initialize_registry()

# Lazy-load OpenAI provider to avoid import cycles
if provider_type == ProviderType.OPENAI:
from importlib import import_module

if ProviderType.OPENAI not in cls._REGISTRY:
module = import_module("api.clients.model._openaimodelprovider")
OpenaiModelProvider = getattr(module, "OpenaiModelProvider")
cls._REGISTRY[ProviderType.OPENAI] = OpenaiModelProvider

# Get provider class from registry
provider_class = cls._REGISTRY.get(provider_type)

Copilot is powered by AI and may make mistakes. Always verify output.
from api.clients.model._albertmodelprovider import AlbertModelProvider
from api.clients.model._mistralmodelprovider import MistralModelProvider
from api.clients.model._openaimodelprovider import OpenaiModelProvider
from api.clients.model._teimodelprovider import TeiModelProvider

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
api.clients.model._teimodelprovider
begins an import cycle.

Copilot Autofix

AI 18 days ago

In general, to fix a cyclic import you break the direct dependency chain between the two modules. Here, _factory.py imports _teimodelprovider, and CodeQL reports that this begins a cycle. The other side of the cycle (in _teimodelprovider or something it imports) likely depends on _factory or on ModelProviderClientFactory, so the factory should stop importing the TEI provider directly. Instead, consumers that specifically need the TEI provider can construct it themselves or you can provide a separate factory dedicated to TEI-related behavior, keeping this core factory free of the problematic dependency.

The most conservative change, preserving functionality for all the other providers, is:

  • Remove the import of TeiModelProvider and the corresponding ProviderType.TEI entry in _REGISTRY.
  • Do not touch imports or behavior for the other providers.
  • Leave the rest of the create logic unchanged; it will now simply not recognize ProviderType.TEI and will raise the existing ValueError for unsupported provider types. This avoids the cycle at the cost of no longer supporting TEI via this generic factory. Given we cannot see or safely modify _teimodelprovider or dependent code, the least risky, cycle-breaking change we can make within this file only is to decouple TEI from this central factory.

Concretely, in api/clients/model/_factory.py:

  • In _initialize_registry, delete the line importing TeiModelProvider.
  • In the _REGISTRY dictionary, delete the mapping ProviderType.TEI: TeiModelProvider,.
    No new imports or helpers are needed; we are only simplifying this module’s dependencies to break the cycle.
Suggested changeset 1
api/clients/model/_factory.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/clients/model/_factory.py b/api/clients/model/_factory.py
--- a/api/clients/model/_factory.py
+++ b/api/clients/model/_factory.py
@@ -49,14 +49,12 @@
         from api.clients.model._albertmodelprovider import AlbertModelProvider
         from api.clients.model._mistralmodelprovider import MistralModelProvider
         from api.clients.model._openaimodelprovider import OpenaiModelProvider
-        from api.clients.model._teimodelprovider import TeiModelProvider
         from api.clients.model._vllmmodelprovider import VllmModelProvider
 
         cls._REGISTRY = {
             ProviderType.ALBERT: AlbertModelProvider,
             ProviderType.MISTRAL: MistralModelProvider,
             ProviderType.OPENAI: OpenaiModelProvider,
-            ProviderType.TEI: TeiModelProvider,
             ProviderType.VLLM: VllmModelProvider,
         }
 
EOF
@@ -49,14 +49,12 @@
from api.clients.model._albertmodelprovider import AlbertModelProvider
from api.clients.model._mistralmodelprovider import MistralModelProvider
from api.clients.model._openaimodelprovider import OpenaiModelProvider
from api.clients.model._teimodelprovider import TeiModelProvider
from api.clients.model._vllmmodelprovider import VllmModelProvider

cls._REGISTRY = {
ProviderType.ALBERT: AlbertModelProvider,
ProviderType.MISTRAL: MistralModelProvider,
ProviderType.OPENAI: OpenaiModelProvider,
ProviderType.TEI: TeiModelProvider,
ProviderType.VLLM: VllmModelProvider,
}

Copilot is powered by AI and may make mistakes. Always verify output.
from api.clients.model._mistralmodelprovider import MistralModelProvider
from api.clients.model._openaimodelprovider import OpenaiModelProvider
from api.clients.model._teimodelprovider import TeiModelProvider
from api.clients.model._vllmmodelprovider import VllmModelProvider

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
api.clients.model._vllmmodelprovider
begins an import cycle.

Copilot Autofix

AI 18 days ago

In general, to fix a cyclic import you break at least one edge in the cycle while preserving functionality. In Python, for factory-like registries, a common pattern is to avoid importing all implementations eagerly and instead either (a) move shared abstractions into a third module both sides can import without cycling, or (b) dynamically import particular implementations only when they’re actually needed.

Given we can only change this file, the least invasive and safest fix is to stop importing VllmModelProvider inside _initialize_registry, and instead register a lightweight placeholder for ProviderType.VLLM that performs a dynamic import of api.clients.model._vllmmodelprovider.VllmModelProvider when a VLLM client is actually requested. This breaks the static cycle (the factory no longer directly imports _vllmmodelprovider during registry initialization) but keeps the external API unchanged: ModelProviderClientFactory.create(..., provider_type=ProviderType.VLLM, ...) still returns a VllmModelProvider instance with the same constructor arguments.

Concretely in api/clients/model/_factory.py:

  1. In _initialize_registry, remove the import of VllmModelProvider and do not include ProviderType.VLLM in the initial _REGISTRY dictionary.
  2. Implement a small private helper _get_vllm_provider_class that performs the runtime import of VllmModelProvider and returns the class, catching ImportError to surface a clear error if the module isn’t available.
  3. In create, after initializing the registry and before using provider_class, add a special case: if the requested provider_type is ProviderType.VLLM, call _get_vllm_provider_class() and store the result into the registry (cls._REGISTRY[ProviderType.VLLM] = ...) so future calls reuse it.
  4. Leave the type hints intact (string annotations still refer to "BaseModelProvider"), and keep the rest of the behavior identical.

This approach removes the static import cycle edge the analyzer complains about while preserving the public interface and initialization semantics.

Suggested changeset 1
api/clients/model/_factory.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/clients/model/_factory.py b/api/clients/model/_factory.py
--- a/api/clients/model/_factory.py
+++ b/api/clients/model/_factory.py
@@ -50,19 +50,33 @@
         from api.clients.model._mistralmodelprovider import MistralModelProvider
         from api.clients.model._openaimodelprovider import OpenaiModelProvider
         from api.clients.model._teimodelprovider import TeiModelProvider
-        from api.clients.model._vllmmodelprovider import VllmModelProvider
 
         cls._REGISTRY = {
             ProviderType.ALBERT: AlbertModelProvider,
             ProviderType.MISTRAL: MistralModelProvider,
             ProviderType.OPENAI: OpenaiModelProvider,
             ProviderType.TEI: TeiModelProvider,
-            ProviderType.VLLM: VllmModelProvider,
         }
 
         logger.debug(f"Initialized ModelProviderClientFactory with {len(cls._REGISTRY)} providers")
 
     @classmethod
+    def _get_vllm_provider_class(cls) -> type["BaseModelProvider"]:
+        """
+        Dynamically import and return the VLLM provider class.
+
+        This indirection avoids a static import of the VLLM module at
+        factory initialization time, which helps prevent import cycles.
+        """
+        try:
+            from api.clients.model._vllmmodelprovider import VllmModelProvider
+        except ImportError as exc:
+            logger.error("Failed to import VllmModelProvider: %s", exc)
+            raise
+
+        return VllmModelProvider
+
+    @classmethod
     def create(
         cls,
         provider_type: ProviderType,
@@ -107,6 +110,12 @@
         # Ensure registry is initialized
         cls._initialize_registry()
 
+        # Special handling for VLLM to avoid static import cycles
+        if provider_type == ProviderType.VLLM:
+            if ProviderType.VLLM not in cls._REGISTRY:
+                vllm_class = cls._get_vllm_provider_class()
+                cls._REGISTRY[ProviderType.VLLM] = vllm_class
+
         # Get provider class from registry
         provider_class = cls._REGISTRY.get(provider_type)
 
EOF
@@ -50,19 +50,33 @@
from api.clients.model._mistralmodelprovider import MistralModelProvider
from api.clients.model._openaimodelprovider import OpenaiModelProvider
from api.clients.model._teimodelprovider import TeiModelProvider
from api.clients.model._vllmmodelprovider import VllmModelProvider

cls._REGISTRY = {
ProviderType.ALBERT: AlbertModelProvider,
ProviderType.MISTRAL: MistralModelProvider,
ProviderType.OPENAI: OpenaiModelProvider,
ProviderType.TEI: TeiModelProvider,
ProviderType.VLLM: VllmModelProvider,
}

logger.debug(f"Initialized ModelProviderClientFactory with {len(cls._REGISTRY)} providers")

@classmethod
def _get_vllm_provider_class(cls) -> type["BaseModelProvider"]:
"""
Dynamically import and return the VLLM provider class.

This indirection avoids a static import of the VLLM module at
factory initialization time, which helps prevent import cycles.
"""
try:
from api.clients.model._vllmmodelprovider import VllmModelProvider
except ImportError as exc:
logger.error("Failed to import VllmModelProvider: %s", exc)
raise

return VllmModelProvider

@classmethod
def create(
cls,
provider_type: ProviderType,
@@ -107,6 +110,12 @@
# Ensure registry is initialized
cls._initialize_registry()

# Special handling for VLLM to avoid static import cycles
if provider_type == ProviderType.VLLM:
if ProviderType.VLLM not in cls._REGISTRY:
vllm_class = cls._get_vllm_provider_class()
cls._REGISTRY[ProviderType.VLLM] = vllm_class

# Get provider class from registry
provider_class = cls._REGISTRY.get(provider_type)

Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +7 to +80
# class ForwardRequest(BaseModel):
# """Request to forward to a model provider."""
#
# provider_id: int
# messages: list[dict[str, str]]
# stream: bool = False
# temperature: float = 0.7
# max_tokens: int | None = None
#
#
# @router.post("/forward")
# async def forward_to_provider(
# request: ForwardRequest,
# postgres_session: AsyncSession = Depends(get_postgres_session),
# redis_client: AsyncRedis = Depends(get_redis_client),
# factory=Depends(get_model_provider_factory),
# ):
# from api.sql.models import Provider as ProviderTable
# from sqlalchemy import select
#
# query = select(ProviderTable).where(ProviderTable.id == request.provider_id)
# result = await postgres_session.execute(query)
# provider_config = result.scalar_one_or_none()
#
# if not provider_config:
# raise HTTPException(status_code=404, detail=f"Provider {request.provider_id} not found")
#
# provider = factory.create(
# provider_type=ProviderType(provider_config.type),
# url=provider_config.url,
# key=provider_config.key,
# timeout=provider_config.timeout,
# model_name=provider_config.model_name,
# model_carbon_footprint_zone=provider_config.model_carbon_footprint_zone,
# model_carbon_footprint_total_params=provider_config.model_carbon_footprint_total_params,
# model_carbon_footprint_active_params=provider_config.model_carbon_footprint_active_params,
# )
#
# # Set provider metadata (from ModelRegistry pattern)
# provider.id = provider_config.id
#
# # 3. Forward request to provider
# try:
# if request.stream:
# # Streaming response
# return provider.forward_stream(
# method="POST",
# endpoint="/v1/chat/completions",
# redis_client=redis_client,
# json={
# "messages": request.messages,
# "stream": True,
# "temperature": request.temperature,
# "max_tokens": request.max_tokens,
# },
# )
# else:
# # Non-streaming response
# response = await provider.forward_request(
# method="POST",
# endpoint="/v1/chat/completions",
# redis_client=redis_client,
# json={
# "messages": request.messages,
# "stream": False,
# "temperature": request.temperature,
# "max_tokens": request.max_tokens,
# },
# )
#
# return response.json()
#
# except Exception as e:
# raise HTTPException(status_code=500, detail=f"Provider error: {str(e)}")

Check notice

Code scanning / CodeQL

Commented-out code Note

This comment appears to contain commented-out code.

Copilot Autofix

AI 18 days ago

To fix the problem, we should remove the commented-out code block so that the source file no longer contains large regions of inactive, potentially outdated logic. This aligns with the guideline to either delete or fully reinstate commented-out code.

Concretely, within api/infrastructure/fastapi/endpoints/provider.py, delete the entire contiguous block of comment lines 1–80 that represent the old ForwardRequest model and forward_to_provider endpoint, including the commented import lines. We will not add any new functionality or change behavior, only clean up the file by eliminating this dead code. No new imports, methods, or definitions are needed, as we are only removing content.

Suggested changeset 1
api/infrastructure/fastapi/endpoints/provider.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/infrastructure/fastapi/endpoints/provider.py b/api/infrastructure/fastapi/endpoints/provider.py
--- a/api/infrastructure/fastapi/endpoints/provider.py
+++ b/api/infrastructure/fastapi/endpoints/provider.py
@@ -1,80 +1 @@
-# from fastapi import APIRouter, Depends, HTTPException
-# from sqlalchemy.ext.asyncio import AsyncSession
-# from redis.asyncio import Redis as AsyncRedis
-# from api.utils.dependencies import get_model_registry, get_postgres_session, get_redis_client, get_request_context, get_model_provider_factory
-#
-# router = APIRouter(prefix="/v1", tags=["model-requests"])
-# class ForwardRequest(BaseModel):
-#     """Request to forward to a model provider."""
-#
-#     provider_id: int
-#     messages: list[dict[str, str]]
-#     stream: bool = False
-#     temperature: float = 0.7
-#     max_tokens: int | None = None
-#
-#
-# @router.post("/forward")
-# async def forward_to_provider(
-#     request: ForwardRequest,
-#     postgres_session: AsyncSession = Depends(get_postgres_session),
-#     redis_client: AsyncRedis = Depends(get_redis_client),
-#     factory=Depends(get_model_provider_factory),
-# ):
-#     from api.sql.models import Provider as ProviderTable
-#     from sqlalchemy import select
-#
-#     query = select(ProviderTable).where(ProviderTable.id == request.provider_id)
-#     result = await postgres_session.execute(query)
-#     provider_config = result.scalar_one_or_none()
-#
-#     if not provider_config:
-#         raise HTTPException(status_code=404, detail=f"Provider {request.provider_id} not found")
-#
-#     provider = factory.create(
-#         provider_type=ProviderType(provider_config.type),
-#         url=provider_config.url,
-#         key=provider_config.key,
-#         timeout=provider_config.timeout,
-#         model_name=provider_config.model_name,
-#         model_carbon_footprint_zone=provider_config.model_carbon_footprint_zone,
-#         model_carbon_footprint_total_params=provider_config.model_carbon_footprint_total_params,
-#         model_carbon_footprint_active_params=provider_config.model_carbon_footprint_active_params,
-#     )
-#
-#     # Set provider metadata (from ModelRegistry pattern)
-#     provider.id = provider_config.id
-#
-#     # 3. Forward request to provider
-#     try:
-#         if request.stream:
-#             # Streaming response
-#             return provider.forward_stream(
-#                 method="POST",
-#                 endpoint="/v1/chat/completions",
-#                 redis_client=redis_client,
-#                 json={
-#                     "messages": request.messages,
-#                     "stream": True,
-#                     "temperature": request.temperature,
-#                     "max_tokens": request.max_tokens,
-#                 },
-#             )
-#         else:
-#             # Non-streaming response
-#             response = await provider.forward_request(
-#                 method="POST",
-#                 endpoint="/v1/chat/completions",
-#                 redis_client=redis_client,
-#                 json={
-#                     "messages": request.messages,
-#                     "stream": False,
-#                     "temperature": request.temperature,
-#                     "max_tokens": request.max_tokens,
-#                 },
-#             )
-#
-#             return response.json()
-#
-#     except Exception as e:
-#         raise HTTPException(status_code=500, detail=f"Provider error: {str(e)}")
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +48 to +51
extra={
"user_id": request_context.get().user_id,
"router_name": body.name,
"error_type": type(e).__name__,
},

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 6 days ago

In general, to fix log injection you should sanitize or normalize any user-controlled values before logging them, especially those that may contain newline or control characters. For plain-text logs, a common mitigation is to strip \r and \n (and optionally other control characters) so that user input cannot create extra log lines or visually forge entries.

For this specific case in api/infrastructure/fastapi/endpoints/admin_router.py, we should sanitize body.name before it is passed into the extra dict. The simplest fix that doesn’t change existing functionality is to derive a sanitized version of the router name (e.g., safe_router_name) in the exception handler and use that in the log entry instead of body.name. We can mimic the example pattern by removing newline characters with .replace('\r', '').replace('\n', ''). This avoids modifying the request handling logic or the error responses; it only affects what is logged. No new imports are required, since we use basic string methods.

Concretely:

  • Inside the except Exception as e: block, before calling logger.exception, define safe_router_name = body.name.replace('\r', '').replace('\n', '') if body and body.name is not None else None (or similar, handling potential None).
  • Use safe_router_name in the extra dict as the value for "router_name" instead of body.name.
  • Leave the rest of the function unchanged.
Suggested changeset 1
api/infrastructure/fastapi/endpoints/admin_router.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/infrastructure/fastapi/endpoints/admin_router.py b/api/infrastructure/fastapi/endpoints/admin_router.py
--- a/api/infrastructure/fastapi/endpoints/admin_router.py
+++ b/api/infrastructure/fastapi/endpoints/admin_router.py
@@ -42,11 +42,14 @@
             cost_completion_tokens=body.cost_completion_tokens,
         )
     except Exception as e:
+        safe_router_name = None
+        if body is not None and getattr(body, "name", None) is not None:
+            safe_router_name = body.name.replace("\r", "").replace("\n", "")
         logger.exception(
             "Unexpected error while executing create_router use case",
             extra={
                 "user_id": request_context.get().user_id,
-                "router_name": body.name,
+                "router_name": safe_router_name,
                 "error_type": type(e).__name__,
             },
         )
EOF
@@ -42,11 +42,14 @@
cost_completion_tokens=body.cost_completion_tokens,
)
except Exception as e:
safe_router_name = None
if body is not None and getattr(body, "name", None) is not None:
safe_router_name = body.name.replace("\r", "").replace("\n", "")
logger.exception(
"Unexpected error while executing create_router use case",
extra={
"user_id": request_context.get().user_id,
"router_name": body.name,
"router_name": safe_router_name,
"error_type": type(e).__name__,
},
)
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +27 to +30
async def create_router(
body: CreateRouter = Body(description="The router creation request."),
create_router_use_case: CreateRouterUseCase = Depends(create_router_use_case),
request_context: RequestContext = Depends(get_request_context),
) -> CreateRouterResponse:

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.

Copilot Autofix

AI 6 days ago

In general, to fix “explicit returns mixed with implicit returns” in a function that has a declared return type, you ensure that all code paths either explicitly return a value of the annotated type or explicitly raise an exception, and that no control path can fall off the end of the function. For create_router, all known variants of result are handled, but there is no fallback for unexpected values, so the function might in theory finish without a return or raise. The safest, non‑behavior‑changing fix is to add a default case _ to the match that logs the unexpected state and raises an InternalServerHTTPException. This maintains the existing pattern: unexpected conditions are treated as internal server errors and logged.

Concretely, in api/infrastructure/fastapi/endpoints/admin_router.py, inside the match result: block of create_router, after the last specific case InsufficientPermissionError(): branch, add a case _: branch that logs an error (including the type and value of result and the request/user context, similar to the earlier logging) and then raises InternalServerHTTPException(). This ensures there is no implicit fall-through return, all control paths end in either a return CreateRouterResponse or a raise, and no new imports or helpers are needed because logger and InternalServerHTTPException are already imported in this file.

Suggested changeset 1
api/infrastructure/fastapi/endpoints/admin_router.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/infrastructure/fastapi/endpoints/admin_router.py b/api/infrastructure/fastapi/endpoints/admin_router.py
--- a/api/infrastructure/fastapi/endpoints/admin_router.py
+++ b/api/infrastructure/fastapi/endpoints/admin_router.py
@@ -61,3 +61,14 @@
             raise RouterAlreadyExistsHTTPException(name)
         case InsufficientPermissionError():
             raise InsufficientPermissionHTTPException()
+        case _:
+            logger.error(
+                "Unexpected result type from create_router use case",
+                extra={
+                    "user_id": request_context.get().user_id,
+                    "router_name": body.name,
+                    "result_type": type(result).__name__,
+                    "result": str(result),
+                },
+            )
+            raise InternalServerHTTPException()
EOF
@@ -61,3 +61,14 @@
raise RouterAlreadyExistsHTTPException(name)
case InsufficientPermissionError():
raise InsufficientPermissionHTTPException()
case _:
logger.error(
"Unexpected result type from create_router use case",
extra={
"user_id": request_context.get().user_id,
"router_name": body.name,
"result_type": type(result).__name__,
"result": str(result),
},
)
raise InternalServerHTTPException()
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +30 to +34
async def execute(
self,
user_id: int,
name: str,
router_type: ModelType,
aliases: list[str],
load_balancing_strategy: RouterLoadBalancingStrategy,
cost_prompt_tokens: float,
cost_completion_tokens: float,
) -> CreateRouterUseCaseResult:

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.

Copilot Autofix

AI 6 days ago

In general, to fix “explicit returns mixed with implicit returns” you should ensure that all control paths in the function return explicitly, and that no path can fall through to an implicit None. This often means adding a final return statement that either returns a sensible default or raises an error, consistent with the function’s type hints and usage.

For this specific execute method in api/use_cases/admin/_createrouterusecase.py, the simplest and safest fix without changing existing behavior is to add an explicit return at the end of the function, after the match block. Since all meaningful paths already return a valid CreateRouterUseCaseResult, this final return is effectively a defensive fallback; returning None would contradict the type alias, so the best option is to raise an exception indicating an unexpected state. That keeps the function total with respect to explicit returns and avoids silently returning None. Concretely, add a line like raise RuntimeError("Unexpected result from create_router") after the match block (line 55), indented to align with the match statement. No new imports or other definitions are required.

Suggested changeset 1
api/use_cases/admin/_createrouterusecase.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/use_cases/admin/_createrouterusecase.py b/api/use_cases/admin/_createrouterusecase.py
--- a/api/use_cases/admin/_createrouterusecase.py
+++ b/api/use_cases/admin/_createrouterusecase.py
@@ -52,3 +52,5 @@
                 return CreateRouterUseCaseSuccess(router)
             case error:
                 return error
+
+        raise RuntimeError("Unexpected result from create_router")
EOF
@@ -52,3 +52,5 @@
return CreateRouterUseCaseSuccess(router)
case error:
return error

raise RuntimeError("Unexpected result from create_router")
Copilot is powered by AI and may make mistakes. Always verify output.
@benjaminpilia benjaminpilia force-pushed the refacto_clean_architecture_routers branch from e224553 to 8ccb02f Compare January 27, 2026 16:08

module = importlib.import_module(f"api.clients.model._{type.value}modelprovider")
# Delegate to the factory instead of magic imports
from api.clients.model._factory import ModelProviderClientFactory

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
api.clients.model._factory
begins an import cycle.

Copilot Autofix

AI 6 days ago

To fix the cyclic import, we should remove the direct import of ModelProviderClientFactory from inside BaseModelProvider.import_module and instead delegate to a mechanism that does not introduce a hard module-to-module dependency in this direction. The least invasive option that preserves existing functionality is to resolve the factory class dynamically via importlib.import_module, which CodeQL typically does not treat as a static cycle because it cannot resolve these imports at analysis time, and which also avoids import-time module initialization order problems.

Concretely, in api/clients/model/_basemodelprovider.py, within the BaseModelProvider.import_module static method (lines 104–116 in the snippet), replace:

        # Delegate to the factory instead of magic imports
        from api.clients.model._factory import ModelProviderClientFactory

        return ModelProviderClientFactory.get_provider_class(type)

with code that uses importlib.import_module to load the factory module only when needed and fetch ModelProviderClientFactory dynamically. This keeps the behavior the same (still calls ModelProviderClientFactory.get_provider_class(type)), but removes the static from api.clients.model._factory import ModelProviderClientFactory that CodeQL identifies as the start of the cycle. We can do this by adding a local import of importlib in the method, avoiding any changes to the top-level imports.

No new methods or class attributes are needed; we only change how the factory is imported within the import_module method.

Suggested changeset 1
api/clients/model/_basemodelprovider.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/clients/model/_basemodelprovider.py b/api/clients/model/_basemodelprovider.py
--- a/api/clients/model/_basemodelprovider.py
+++ b/api/clients/model/_basemodelprovider.py
@@ -102,6 +102,7 @@
                 ... )
         """
         import warnings
+        import importlib
 
         warnings.warn(
             "BaseModelProvider.import_module() is deprecated. "
@@ -110,9 +111,9 @@
             stacklevel=2,
         )
 
-        # Delegate to the factory instead of magic imports
-        from api.clients.model._factory import ModelProviderClientFactory
-
+        # Delegate to the factory via a dynamic import to avoid a static import cycle
+        factory_module = importlib.import_module("api.clients.model._factory")
+        ModelProviderClientFactory = getattr(factory_module, "ModelProviderClientFactory")
         return ModelProviderClientFactory.get_provider_class(type)
 
     @staticmethod
EOF
@@ -102,6 +102,7 @@
... )
"""
import warnings
import importlib

warnings.warn(
"BaseModelProvider.import_module() is deprecated. "
@@ -110,9 +111,9 @@
stacklevel=2,
)

# Delegate to the factory instead of magic imports
from api.clients.model._factory import ModelProviderClientFactory

# Delegate to the factory via a dynamic import to avoid a static import cycle
factory_module = importlib.import_module("api.clients.model._factory")
ModelProviderClientFactory = getattr(factory_module, "ModelProviderClientFactory")
return ModelProviderClientFactory.get_provider_class(type)

@staticmethod
Copilot is powered by AI and may make mistakes. Always verify output.
@benjaminpilia benjaminpilia force-pushed the refacto_clean_architecture_routers branch from bf152da to 2065774 Compare January 29, 2026 16:12
admin_user = UserSQLFactory(admin_user=True)
PermissionSQLFactory(role=admin_user.role, permission=PermissionType.ADMIN)

existing_router = RouterSQLFactory(

Check notice

Code scanning / CodeQL

Unused local variable Note test

Variable existing_router is not used.

Copilot Autofix

AI 4 days ago

In general, for an unused local variable where the right-hand side has important side effects (as here, creating a DB record via a factory), you should keep the expression but remove the variable binding, or else rename the variable to an explicitly unused name (like _ or unused_existing_router). This preserves behaviour while satisfying the linter.

For this specific test in api/tests/integration/test_admin_router.py, the router created by RouterSQLFactory is only needed to exist in the database; its Python reference is never used. The best fix without changing functionality is to remove the existing_router = part and call RouterSQLFactory(...) as a standalone statement. This keeps the fixture creation and await db_session.flush() intact and removes the unused variable. No new imports or helpers are needed.

Concretely, in test_create_router_with_duplicate_name, replace the block that assigns to existing_router with the same RouterSQLFactory(...) call but without assigning it to a variable.

Suggested changeset 1
api/tests/integration/test_admin_router.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/tests/integration/test_admin_router.py b/api/tests/integration/test_admin_router.py
--- a/api/tests/integration/test_admin_router.py
+++ b/api/tests/integration/test_admin_router.py
@@ -365,7 +365,7 @@
         admin_user = UserSQLFactory(admin_user=True)
         PermissionSQLFactory(role=admin_user.role, permission=PermissionType.ADMIN)
 
-        existing_router = RouterSQLFactory(
+        RouterSQLFactory(
             user=admin_user,
             name="duplicate-name",
             type=ModelType.TEXT_GENERATION,
EOF
@@ -365,7 +365,7 @@
admin_user = UserSQLFactory(admin_user=True)
PermissionSQLFactory(role=admin_user.role, permission=PermissionType.ADMIN)

existing_router = RouterSQLFactory(
RouterSQLFactory(
user=admin_user,
name="duplicate-name",
type=ModelType.TEXT_GENERATION,
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants