Skip to content

Commit 9927124

Browse files
fix(obs): read litellm's provider list defensively, and say so if it goes
CI's lint job runs pyright, which `ruff check` alone does not: two errors, both mine — `"openai_compatible_providers" is not exported from module "litellm"` (reportPrivateImportUsage). Pyright is right. The attribute is real at runtime but is not in litellm's `__all__`, so it is not a promised export and a future release may rename or drop it. Read via `getattr` now, which fixes the lint and, more to the point, degrades instead of raising if it does disappear. But degrading quietly would silently re-introduce the exact double counting the previous commit fixed: without litellm's list only five providers are known to reach the model over the OpenAI client, so groq, deepseek, xai and ~50 others would look native again and be recorded twice — once here and once by the OpenAI client instrumentor. So the fallback warns and names what it can no longer recognise. Covered by a test that deletes the attribute and asserts both halves: azure still resolves, groq no longer does, and the warning fires. Verified with the real CI entry points rather than a subset: `./scripts/lint` (ruff over the whole tree, pyright, import check) and `./scripts/check-slim-deps` both clean. Tests 34 in this file, 1714 repo-wide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3ba999c commit 9927124

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

‎src/agentex/lib/core/adapters/llm/_genai_metrics.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,32 @@ def _over_openai_client(provider: str) -> bool:
6262
"""
6363
global _openai_client_providers
6464
if _openai_client_providers is _OPENAI_CLIENT_PROVIDERS_UNRESOLVED:
65+
compatible: Any = None
6566
try:
6667
import litellm
6768

69+
# Read via getattr: the attribute is not in litellm's __all__, so it is
70+
# not a promised export and a future release may rename or drop it.
71+
compatible = getattr(litellm, "openai_compatible_providers", None)
72+
except Exception: # pragma: no cover - litellm is a hard dependency
73+
compatible = None
74+
75+
if compatible:
6876
_openai_client_providers = (
69-
frozenset(litellm.openai_compatible_providers)
70-
| _EXTRA_OPENAI_CLIENT_PROVIDERS
77+
frozenset(compatible) | _EXTRA_OPENAI_CLIENT_PROVIDERS
78+
)
79+
else:
80+
# Degrading quietly here would re-introduce the double counting this
81+
# function exists to prevent: every openai-compatible provider would look
82+
# native again and be recorded twice. Say so rather than drift.
83+
logger.warning(
84+
"litellm no longer exposes openai_compatible_providers, so GenAI "
85+
"metrics can only recognise %d providers as reaching the model over "
86+
"the OpenAI client. Calls to openai-compatible providers such as "
87+
"groq or deepseek may now be counted twice, once here and once by "
88+
"the OpenAI client instrumentor.",
89+
len(_EXTRA_OPENAI_CLIENT_PROVIDERS),
7190
)
72-
except Exception: # pragma: no cover - litellm is a hard dependency
7391
_openai_client_providers = _EXTRA_OPENAI_CLIENT_PROVIDERS
7492
return provider in _openai_client_providers
7593

‎src/agentex/lib/core/adapters/llm/tests/test_genai_metrics.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,28 @@ def test_the_provider_list_agrees_with_litellm(self):
123123

124124
from agentex.lib.core.adapters.llm._genai_metrics import _over_openai_client
125125

126-
for provider in list(litellm.openai_compatible_providers)[:20]:
126+
for provider in list(getattr(litellm, "openai_compatible_providers", []))[:20]:
127127
assert _over_openai_client(provider), provider
128128
for provider in ("anthropic", "bedrock", "vertex_ai", "gemini"):
129129
assert not _over_openai_client(provider), provider
130130

131+
def test_losing_litellms_list_is_not_silent(self, monkeypatch, caplog):
132+
"""`openai_compatible_providers` is not in litellm's __all__, so it is read
133+
defensively. But degrading quietly would re-introduce the double counting this
134+
whole function exists to prevent, so the fallback has to be audible."""
135+
import litellm
136+
137+
monkeypatch.delattr(litellm, "openai_compatible_providers", raising=False)
138+
_genai_metrics._reset_for_tests()
139+
140+
with caplog.at_level("WARNING", logger=_genai_metrics.logger.name):
141+
# The five explicit ones still work; the ~50 from litellm no longer do.
142+
assert _genai_metrics._over_openai_client("azure") is True
143+
assert _genai_metrics._over_openai_client("groq") is False
144+
assert any("openai_compatible_providers" in r.message for r in caplog.records), [
145+
r.message for r in caplog.records
146+
]
147+
131148
def test_an_unresolvable_model_does_not_spam_stdout(self):
132149
"""litellm prints a red "Provider List" banner to STDOUT (not logging, so it
133150
cannot be filtered) every time resolution fails. Uncached, an agent on a model

0 commit comments

Comments
 (0)