1111if TYPE_CHECKING :
1212 from integrations .llm_cli .registry import CLIProviderRegistration
1313
14- from anthropic import BadRequestError as AnthropicBadRequestError # noqa: F401
15- from anthropic import NotFoundError # noqa: F401
16- from openai import APITimeoutError as OpenAITimeoutError # noqa: F401
17- from openai import BadRequestError as OpenAIBadRequestError # noqa: F401
18- from openai import RateLimitError as OpenAIRateLimitError # noqa: F401
14+ from anthropic import BadRequestError as AnthropicBadRequestError
15+ from anthropic import NotFoundError
16+ from openai import APITimeoutError as OpenAITimeoutError
17+ from openai import BadRequestError as OpenAIBadRequestError
18+ from openai import RateLimitError as OpenAIRateLimitError
1919from pydantic import BaseModel , ValidationError
2020
2121from config .config import (
2525)
2626from config .llm_auth .auth_method import effective_llm_provider , get_configured_llm_auth_method
2727from core .domain .types .root_cause_categories import VALID_ROOT_CAUSE_CATEGORIES
28- from core .llm .openai_chat_completions import _RETRY_MAX_ATTEMPTS # noqa: F401
28+ from core .llm .openai_chat_completions import _RETRY_MAX_ATTEMPTS
2929from core .llm .openai_compat_providers import (
30- OPENAI_COMPATIBLE_PROVIDERS ,
3130 ModelType ,
3231 is_openai_compat_provider ,
3332 resolve_openai_compat_provider ,
3433)
35- from core .llm .sdk .llm_clients import (
36- BedrockLLMClient ,
37- LLMClient ,
38- OpenAILLMClient ,
39- _format_anthropic_retry_error , # noqa: F401
40- _format_openai_connection_error , # noqa: F401
41- _is_anthropic_bedrock_model , # noqa: F401
42- )
34+ from core .llm .provider_credentials import resolve_llm_api_key
4335from core .llm .transport_mode import current_llm_transport , use_litellm_transport
4436from core .llm .types import LLMResponse
4537from core .llm .usage import UsageHook , emit_usage , set_usage_hook
46- from core .llm .usage import coerce_usage_tokens as _coerce_usage_tokens # noqa: F401
38+
39+ if TYPE_CHECKING :
40+ from core .llm .sdk .llm_clients import BedrockLLMClient , LLMClient , OpenAILLMClient
4741
4842__all__ = [
4943 "LLMClient" ,
6559 "resolve_llm_api_key" ,
6660]
6761
68- from core .llm .provider_credentials import resolve_llm_api_key
62+ _SDK_EXPORTS = frozenset (
63+ {
64+ "LLMClient" ,
65+ "OpenAILLMClient" ,
66+ "BedrockLLMClient" ,
67+ "_format_anthropic_retry_error" ,
68+ "_format_openai_connection_error" ,
69+ "_is_anthropic_bedrock_model" ,
70+ }
71+ )
72+
73+ # Re-exported for tests (``tests/core/runtime/llm/test_llm_client.py``).
74+ _ = (
75+ AnthropicBadRequestError ,
76+ NotFoundError ,
77+ _RETRY_MAX_ATTEMPTS ,
78+ )
79+
6980
70- _emit_usage = emit_usage
81+ def _sdk_llm_clients_module () -> Any :
82+ from core .llm .sdk import llm_clients as module
7183
72- _OPENAI_COMPATIBLE_PROVIDERS = OPENAI_COMPATIBLE_PROVIDERS
84+ return module
85+
86+
87+ def __getattr__ (name : str ) -> Any :
88+ if name in _SDK_EXPORTS :
89+ return getattr (_sdk_llm_clients_module (), name )
90+ raise AttributeError (f"module { __name__ !r} has no attribute { name !r} " )
7391
7492
7593class SupportsLLMInvoke (Protocol ):
@@ -99,30 +117,41 @@ class RootCauseResult:
99117 remediation_steps : list [str ]
100118
101119
102- _LLMClientType = LLMClient | OpenAILLMClient | BedrockLLMClient | SupportsLLMInvoke
103- _llm : _LLMClientType | None = None
104- _llm_for_classification : _LLMClientType | None = None
105- _llm_for_tools : _LLMClientType | None = None
106- _llm_transport : str | None = None
120+ _LLMClientType = Any
121+
122+
123+ class _LLMSingletonState :
124+ """Mutable holder for cached non-agent LLM clients and transport mode.
125+
126+ Wrapped in a class so transport/client fields are read/written via attribute
127+ access on a stable container, avoiding the ``global`` keyword (which CodeQL's
128+ ``py/unused-global-variable`` rule misreports despite the in-function reads).
129+ """
130+
131+ llm : _LLMClientType | None = None
132+ llm_for_classification : _LLMClientType | None = None
133+ llm_for_tools : _LLMClientType | None = None
134+ transport : str | None = None
135+
136+
137+ _llm_state = _LLMSingletonState ()
107138
108139
109140def reset_llm_singletons () -> None :
110141 """Clear cached LLM clients (tests, benchmarks, alternate configs)."""
111- global _llm , _llm_for_classification , _llm_for_tools , _llm_transport
112- _llm = None
113- _llm_for_classification = None
114- _llm_for_tools = None
115- _llm_transport = None
142+ _llm_state .llm = None
143+ _llm_state .llm_for_classification = None
144+ _llm_state .llm_for_tools = None
145+ _llm_state .transport = None
116146
117147
118148def _ensure_llm_transport_current () -> None :
119- global _llm , _llm_for_classification , _llm_for_tools , _llm_transport
120149 transport = current_llm_transport ()
121- if _llm_transport != transport :
122- _llm = None
123- _llm_for_classification = None
124- _llm_for_tools = None
125- _llm_transport = transport
150+ if _llm_state . transport != transport :
151+ _llm_state . llm = None
152+ _llm_state . llm_for_classification = None
153+ _llm_state . llm_for_tools = None
154+ _llm_state . transport = transport
126155
127156
128157def _get_cli_provider_registration (provider : str ) -> CLIProviderRegistration | None :
@@ -179,15 +208,17 @@ def _fallback_model(provider_prefix: str) -> str | None:
179208
180209 if runtime_provider == "openai" :
181210 config = OPENAI_LLM_CONFIG
182- return OpenAILLMClient (
211+ sdk = _sdk_llm_clients_module ()
212+ return sdk .OpenAILLMClient (
183213 model = _select_model (settings , "openai" , model_type ),
184214 model_fallback = _fallback_model ("openai" ),
185215 max_tokens = config .max_tokens ,
186216 )
187217 elif is_openai_compat_provider (runtime_provider ):
188218 compat = resolve_openai_compat_provider (settings , runtime_provider , model_type )
189219 fallback = _fallback_model (runtime_provider )
190- return OpenAILLMClient (
220+ sdk = _sdk_llm_clients_module ()
221+ return sdk .OpenAILLMClient (
191222 model = compat .model ,
192223 model_fallback = fallback ,
193224 max_tokens = compat .config .max_tokens ,
@@ -199,43 +230,42 @@ def _fallback_model(provider_prefix: str) -> str | None:
199230 elif runtime_provider == "bedrock" :
200231 from config .config import BEDROCK_LLM_CONFIG
201232
202- return BedrockLLMClient (
233+ sdk = _sdk_llm_clients_module ()
234+ return sdk .BedrockLLMClient (
203235 model = _select_model (settings , "bedrock" , model_type ),
204236 max_tokens = BEDROCK_LLM_CONFIG .max_tokens ,
205237 )
206238 else :
207239 config = ANTHROPIC_LLM_CONFIG
208- return LLMClient (
240+ sdk = _sdk_llm_clients_module ()
241+ return sdk .LLMClient (
209242 model = _select_model (settings , "anthropic" , model_type ),
210243 max_tokens = config .max_tokens ,
211244 )
212245
213246
214247def get_llm_for_reasoning () -> _LLMClientType :
215248 """Return the singleton LLM client for complex reasoning tasks."""
216- global _llm
217249 _ensure_llm_transport_current ()
218- if _llm is None :
219- _llm = _create_llm_client (model_type = "reasoning" )
220- return _llm
250+ if _llm_state . llm is None :
251+ _llm_state . llm = _create_llm_client (model_type = "reasoning" )
252+ return _llm_state . llm
221253
222254
223255def get_llm_for_classification () -> _LLMClientType :
224256 """Return the singleton LLM client for the mid-tier classification tier."""
225- global _llm_for_classification
226257 _ensure_llm_transport_current ()
227- if _llm_for_classification is None :
228- _llm_for_classification = _create_llm_client (model_type = "classification" )
229- return _llm_for_classification
258+ if _llm_state . llm_for_classification is None :
259+ _llm_state . llm_for_classification = _create_llm_client (model_type = "classification" )
260+ return _llm_state . llm_for_classification
230261
231262
232263def get_llm_for_tools () -> _LLMClientType :
233264 """Return the singleton lightweight LLM client for tool selection / action planning."""
234- global _llm_for_tools
235265 _ensure_llm_transport_current ()
236- if _llm_for_tools is None :
237- _llm_for_tools = _create_llm_client (model_type = "toolcall" )
238- return _llm_for_tools
266+ if _llm_state . llm_for_tools is None :
267+ _llm_state . llm_for_tools = _create_llm_client (model_type = "toolcall" )
268+ return _llm_state . llm_for_tools
239269
240270
241271def parse_root_cause (response : str ) -> RootCauseResult :
0 commit comments