Name the provider a model call actually reached

DeepSeek and every other OpenAI-compatible API run through OpenAILLM, so
spans and metrics reported them as openai. The provider now comes from the
endpoint: known API hosts map to their gen_ai.provider.name (deepseek,
azure.ai.openai, mistral_ai, ...), and an OpenAI client pointed anywhere
else is openai_compatible. LLM spans also record server.address.
This commit is contained in:
arc53-machine committed 2026-09-24 00:08:53 +01:00
1 parent 6b145d7aad
commit 3156ee37a1
3 files changed
+119 -2

No files matched your search

+4
View File
@@ -128,6 +128,10 @@ class LLMCreator:
# llm.model_id is the upstream name (BYOM resolves it above); stamp
# the canonical id (UUID for BYOM) separately for token_usage.
llm._canonical_model_id = model_id
# The provider plugin that built it: ``openai_compatible`` endpoints
# run through the OpenAI client, so the class alone cannot name them
# in traces (see ``docsgpt.tracing.llm.llm_provider``).
llm._provider_plugin = plugin.name
# Calls to a user's own model are recorded at $0 (see ``docsgpt/usage.py``).
llm._is_byom = model is not None and getattr(model, "source", "builtin") == "user"
return llm
+64 -2
View File
@@ -8,6 +8,7 @@ fallback each get their own span with the provider that actually ran.
from __future__ import annotations
from typing import Any, Dict, Iterable, Optional, Union
from urllib.parse import urlparse
from docsgpt.core.settings import settings
from docsgpt.tracing import core
@@ -16,6 +17,66 @@ from docsgpt.tracing.otel import provider_name, record_llm_metrics
#: LLM attribute a cache wrapper sets when it served the call from Redis.
CACHE_HIT_ATTR = "_trace_cache_hit"
#: API hosts whose provider differs from the client class that calls them
#: (every OpenAI-compatible API runs through ``OpenAILLM``), mapped to their
#: ``gen_ai.provider.name``. Matched on the host or any subdomain of it.
_PROVIDER_HOSTS = (
("api.openai.com", "openai"),
("openai.azure.com", "azure.ai.openai"),
("deepseek.com", "deepseek"),
("mistral.ai", "mistral_ai"),
("x.ai", "x_ai"),
("perplexity.ai", "perplexity"),
("groq.com", "groq"),
("openrouter.ai", "openrouter"),
("novita.ai", "novita"),
("cohere.com", "cohere"),
("cohere.ai", "cohere"),
("together.xyz", "together_ai"),
("together.ai", "together_ai"),
("fireworks.ai", "fireworks"),
("anthropic.com", "anthropic"),
("generativelanguage.googleapis.com", "gcp.gemini"),
)
def _endpoint_host(llm: Any) -> Optional[str]:
url = getattr(llm, "_effective_base_url", None) or getattr(llm, "base_url", None)
if not isinstance(url, str) or not url:
return None
try:
return urlparse(url).hostname
except ValueError:
return None
def llm_provider(llm: Any) -> str:
"""The ``gen_ai.provider.name`` of the API ``llm`` actually calls.
The client class alone is not enough: DeepSeek, a self-hosted vLLM or any
other OpenAI-compatible endpoint runs through ``OpenAILLM``, whose
``provider_name`` is ``openai``. A known API host wins; an OpenAI client
pointed at any other host is reported as ``openai_compatible``.
Args:
llm: The LLM instance making the call.
Returns:
The provider name for spans and metrics.
"""
base = provider_name(getattr(llm, "provider_name", None))
host = _endpoint_host(llm)
if host:
for suffix, name in _PROVIDER_HOSTS:
if host == suffix or host.endswith("." + suffix):
return name
if getattr(llm, "_provider_plugin", None) == "openai_compatible":
return "openai_compatible"
if base == "openai" and host:
# An OpenAI client aimed somewhere other than OpenAI's API.
return "openai_compatible"
return base
def start_llm_span(
llm: Any, model: Optional[str], *, stream: bool, tools: Any = None
@@ -25,7 +86,8 @@ def start_llm_span(
return core.NOOP_SPAN
attributes = {
"gen_ai.operation.name": "chat",
"gen_ai.provider.name": provider_name(getattr(llm, "provider_name", None)),
"gen_ai.provider.name": llm_provider(llm),
"server.address": _endpoint_host(llm),
"gen_ai.request.model": str(model) if model else None,
"docsgpt.token_source": getattr(llm, "_token_usage_source", None) or "agent_stream",
"docsgpt.stream": bool(stream),
@@ -83,7 +145,7 @@ def finish_llm_call(
if not settings.TRACES_ENABLED:
return
record_llm_metrics(
provider=getattr(llm, "provider_name", None),
provider=llm_provider(llm),
model=str(model) if model else None,
input_tokens=call_usage.get("prompt_tokens", 0),
output_tokens=call_usage.get("generated_tokens", 0),
+51
View File
@@ -226,3 +226,54 @@ class TestCacheHits:
first, second = trace.spans
assert first.attributes.get("docsgpt.cache_hit") is None
assert second.attributes["docsgpt.cache_hit"] is True
class TestProviderResolution:
"""The span names the provider actually called, not the client class used."""
@staticmethod
def _llm(provider="openai", base_url=None, plugin=None):
llm = _LLM()
llm.provider_name = provider
if base_url is not None:
llm._effective_base_url = base_url
if plugin is not None:
llm._provider_plugin = plugin
return llm
@pytest.mark.parametrize(
"base_url, plugin, expected",
[
("https://api.deepseek.com/v1", "openai_compatible", "deepseek"),
("https://my-res.openai.azure.com/openai", "openai", "azure.ai.openai"),
("https://api.mistral.ai/v1", "openai_compatible", "mistral_ai"),
("https://api.x.ai/v1", "openai_compatible", "x_ai"),
("https://api.openai.com/v1", "openai", "openai"),
("http://10.0.0.5:8000/v1", "openai_compatible", "openai_compatible"),
("http://127.0.0.1:7899/v1", "openai", "openai_compatible"),
(None, "openai", "openai"),
],
)
def test_provider_from_endpoint(self, base_url, plugin, expected):
from docsgpt.tracing.llm import llm_provider
assert llm_provider(self._llm(base_url=base_url, plugin=plugin)) == expected
def test_native_providers_are_unchanged(self):
from docsgpt.tracing.llm import llm_provider
assert llm_provider(self._llm(provider="anthropic")) == "anthropic"
assert llm_provider(self._llm(provider="google")) == "gcp.gen_ai"
def test_span_and_metrics_use_it_and_record_server_address(self, trace, metrics):
llm = self._llm(base_url="https://api.deepseek.com/v1", plugin="openai_compatible")
@gen_token_usage
def _gen(self, model, messages, stream, tools, **kwargs):
return "x"
_gen(llm, "deepseek-chat", [], False, None)
attrs = trace.spans[0].attributes
assert attrs["gen_ai.provider.name"] == "deepseek"
assert attrs["server.address"] == "api.deepseek.com"
assert metrics.call_args.kwargs["provider"] == "deepseek"