From 76457f529c21799dd210aa99305f1d1118010a6e Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Mon, 17 Mar 2025 15:24:30 -0400 Subject: [PATCH 1/3] feat(api): msp-dictionary-update --- docs/quickstart.md | 6 +- scope3ai/api/tracer.py | 1 + scope3ai/base_tracer.py | 4 ++ scope3ai/constants.py | 92 +++++++++++++++++++++++++-- scope3ai/lib.py | 54 ++++++++-------- scope3ai/tracers/anthropic/chat.py | 6 ++ scope3ai/tracers/cohere/chat.py | 5 ++ scope3ai/tracers/cohere/chat_v2.py | 5 ++ scope3ai/tracers/google_genai/chat.py | 2 + scope3ai/tracers/huggingface/chat.py | 5 ++ scope3ai/tracers/litellm/chat.py | 5 ++ scope3ai/tracers/mistralai/chat.py | 5 ++ scope3ai/tracers/openai/chat.py | 5 ++ tests/test_constants.py | 10 +++ tests/test_lib_metadata.py | 6 +- 15 files changed, 173 insertions(+), 38 deletions(-) create mode 100644 tests/test_constants.py diff --git a/docs/quickstart.md b/docs/quickstart.md index 6652336..3df3110 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -56,15 +56,15 @@ from scope3ai import Scope3AI scope3 = Scope3AI.init() ``` -## Enable Specific Providers +## Enable Specific Providers Clients -By default, all supported providers are enabled if found in available installed +By default, all supported provider clients are enabled if found in available installed libraries. You can specify which ones to enable: ```python scope3 = Scope3AI.init( api_key="YOUR_API_KEY", - providers=["openai", "anthropic", "cohere"] + provider_clients=["openai", "anthropic", "cohere"] ) ``` diff --git a/scope3ai/api/tracer.py b/scope3ai/api/tracer.py index 4bde417..755e6e1 100644 --- a/scope3ai/api/tracer.py +++ b/scope3ai/api/tracer.py @@ -4,6 +4,7 @@ from .typesgen import ImpactResponse, ModeledRow +# TODO Tracer is not BaseTracer? class Tracer: """ Tracer is responsible for tracking and aggregating environmental impact metrics diff --git a/scope3ai/base_tracer.py b/scope3ai/base_tracer.py index f0157e2..ad6e222 100644 --- a/scope3ai/base_tracer.py +++ b/scope3ai/base_tracer.py @@ -1,8 +1,12 @@ from wrapt import wrap_function_wrapper +from scope3ai.constants import CLIENTS + +# TODO Tracer is not BaseTracer? class BaseTracer: wrapper_methods = [] + client: CLIENTS def instrument(self) -> None: for wrapper in self.wrapped_methods: diff --git a/scope3ai/constants.py b/scope3ai/constants.py index 895448d..1d92493 100644 --- a/scope3ai/constants.py +++ b/scope3ai/constants.py @@ -1,12 +1,92 @@ from enum import Enum +from typing import Optional -class PROVIDERS(Enum): - ANTROPIC = "anthropic" +# client is an API provided by managed service providers or third parties to interact with managed service providers +class CLIENTS(Enum): + # some clients hide more than 1 provider, like monsieur Google. We want to distinguish between attaching to a client and sending a provider + GOOGLE_OPENAI = "google-openai" + + OPENAI = "openai" + ANTHROPIC = "anthropic" COHERE = "cohere" + HUGGINGFACE_HUB = "huggingface" + LITELLM = "litellm" # WARN - special + MISTRALAI = "mistral" + RESPONSE = "response" + # TODO - this is full list + # AWS_BEDROCK = "aws-bedrock" + # AZURE_OPENAI = "azure-openai" + # IBM_WATSONX = "ibm-watsonx" + # ORACLE_AI = "oracle-ai" + # ALIBABA_PAI = "alibaba-pai" + # TENCENT_HUNYUAN = "tencent-hunyuan" + # YANDEX_YAGPT = "yandex-yagpt" + # REPLICATE = "replicate" + # AI21 = "ai21" + # TOGETHER = "together" + # ANYSCALE = "anyscale" + # DEEPINFRA = "deepinfra" + # PERPLEXITY = "perplexity" + # GROQ = "groq" + # FIREWORKS = "fireworks" + # FOREFRONT = "forefront" + # NVIDIA_NEMO = "nvidia-nemo" + # STABILITY_AI = "stability-ai" + # META_LLAMA = "meta-llama" + # INFLECTION_AI = "inflection-ai" + # DATABRICKS = "databricks" + # WRITER = "writer" + + +# codependency ref 2E92DAFC-3800-4E36-899B-18E842ADB8E3 https://github.com/scope3data/aiapi +# TODO get from openapi +# providers are APIs provided by managed service providers; since having multiple APIs for one managed service provider is rare (oh hi Google), we just keep calling it all "managed service providers" +class PROVIDERS(Enum): + GOOGLE_GEMINI = "google-gemini" + GOOGLE_VERTEX = "google-vertex" + OPENAI = "openai" - HUGGINGFACE_HUB = "huggingface_hub" - LITELLM = "litellm" - MISTRALAI = "mistralai" + ANTHROPIC = "anthropic" + COHERE = "cohere" + HUGGINGFACE_HUB = "huggingface" + LITELLM = "litellm" # WARN - special + MISTRALAI = "mistral" RESPONSE = "response" - GOOGLE_GENAI = "google_genai" + + +# API to Provider are many to many +# but we assume they x = x for all the providers/clients in PROVIDER_CLIENTS +PROVIDER_TO_CLIENT = { + PROVIDERS.GOOGLE_GEMINI: [CLIENTS.GOOGLE_OPENAI], + PROVIDERS.GOOGLE_VERTEX: [CLIENTS.GOOGLE_OPENAI], + PROVIDERS.OPENAI: [CLIENTS.OPENAI], + PROVIDERS.ANTHROPIC: [CLIENTS.ANTHROPIC], + PROVIDERS.COHERE: [CLIENTS.COHERE], + PROVIDERS.HUGGINGFACE_HUB: [CLIENTS.HUGGINGFACE_HUB], + PROVIDERS.LITELLM: [CLIENTS.LITELLM], + PROVIDERS.MISTRALAI: [CLIENTS.MISTRALAI], + PROVIDERS.RESPONSE: [CLIENTS.RESPONSE], +} + +CLIENT_TO_PROVIDER = { + CLIENTS.GOOGLE_OPENAI: [PROVIDERS.GOOGLE_GEMINI, PROVIDERS.GOOGLE_VERTEX], + CLIENTS.OPENAI: [PROVIDERS.OPENAI], + CLIENTS.ANTHROPIC: [PROVIDERS.ANTHROPIC], + CLIENTS.COHERE: [PROVIDERS.COHERE], + CLIENTS.HUGGINGFACE_HUB: [PROVIDERS.HUGGINGFACE_HUB], + CLIENTS.LITELLM: [PROVIDERS.LITELLM], + CLIENTS.MISTRALAI: [PROVIDERS.MISTRALAI], + CLIENTS.RESPONSE: [PROVIDERS.RESPONSE], +} + + +def try_provider_for_client(client: CLIENTS) -> Optional[PROVIDERS]: + r = CLIENT_TO_PROVIDER.get(client) + if r is None: + # client without provider is a coding error. throw, crash everything + raise ValueError(f"client {client} has no provider") + # not determined or emptiness + if len(r) > 1 or len(r) == 0: + return None + return r[0] diff --git a/scope3ai/lib.py b/scope3ai/lib.py index 29ec440..48d9fa5 100644 --- a/scope3ai/lib.py +++ b/scope3ai/lib.py @@ -13,7 +13,7 @@ from .api.defaults import DEFAULT_API_URL, DEFAULT_APPLICATION_ID from .api.tracer import Tracer from .api.types import ImpactRequest, ImpactResponse, ImpactRow, Scope3AIContext -from .constants import PROVIDERS +from .constants import CLIENTS from .worker import BackgroundWorker logger = logging.getLogger("scope3ai.lib") @@ -83,17 +83,19 @@ def init_response_instrumentor() -> None: _INSTRUMENTS = { - PROVIDERS.ANTROPIC.value: init_anthropic_instrumentor, - PROVIDERS.COHERE.value: init_cohere_instrumentor, - PROVIDERS.OPENAI.value: init_openai_instrumentor, - PROVIDERS.HUGGINGFACE_HUB.value: init_huggingface_hub_instrumentor, - PROVIDERS.GOOGLE_GENAI.value: init_google_genai_instrumentor, - PROVIDERS.LITELLM.value: init_litellm_instrumentor, - PROVIDERS.MISTRALAI.value: init_mistral_v1_instrumentor, - PROVIDERS.RESPONSE.value: init_response_instrumentor, + CLIENTS.ANTHROPIC.value: init_anthropic_instrumentor, + CLIENTS.COHERE.value: init_cohere_instrumentor, + CLIENTS.OPENAI.value: init_openai_instrumentor, + CLIENTS.HUGGINGFACE_HUB.value: init_huggingface_hub_instrumentor, + # TODO current tests use gemini + CLIENTS.GOOGLE_OPENAI.value: init_google_genai_instrumentor, + CLIENTS.LITELLM.value: init_litellm_instrumentor, + CLIENTS.MISTRALAI.value: init_mistral_v1_instrumentor, + CLIENTS.RESPONSE.value: init_response_instrumentor, } -_RE_INIT_PROVIDERS = [PROVIDERS.RESPONSE.value] +# TODO what it means / why reinit is allowed here +_RE_INIT_CLIENTS = [CLIENTS.RESPONSE.value] def generate_id() -> str: @@ -115,7 +117,7 @@ class Scope3AI: _instance: Optional["Scope3AI"] = None _tracer: ContextVar[List[Tracer]] = ContextVar("tracer", default=[]) _worker: Optional[BackgroundWorker] = None - _providers: List[str] = [] + _clients: List[str] = [] _keep_tracers: bool = False def __new__(cls, *args, **kwargs): @@ -141,7 +143,8 @@ def init( api_url: Optional[str] = None, sync_mode: bool = False, enable_debug_logging: bool = False, - providers: Optional[List[str]] = None, + # we have provider_clients and not clients naming here because client also has client_id which is not a [provider] client but a [scope3] client + provider_clients: Optional[List[str]] = None, # metadata for scope3 environment: Optional[str] = None, client_id: Optional[str] = None, @@ -160,8 +163,8 @@ def init( set via `SCOPE3AI_SYNC_MODE` environment variable. Defaults to False. enable_debug_logging (bool, optional): Enable debug level logging. Can be set via `SCOPE3AI_DEBUG_LOGGING` environment variable. Defaults to False. - providers (List[str], optional): List of providers to instrument. If None, - all available providers will be instrumented. + clients (List[str], optional): List of provider clients to instrument. If None, + all available provider clients will be instrumented. environment (str, optional): The environment name (e.g. "production", "staging"). Can be set via `SCOPE3AI_ENVIRONMENT` environment variable. client_id (str, optional): Client identifier for grouping traces. Can be set via @@ -209,13 +212,14 @@ def init( if enable_debug_logging: self._init_logging() - if providers is None: - providers = list(_INSTRUMENTS.keys()) + clients = provider_clients + if clients is None: + clients = list(_INSTRUMENTS.keys()) http_client_options = {"api_key": self.api_key, "api_url": self.api_url} self._sync_client = Client(**http_client_options) self._async_client = AsyncClient(**http_client_options) - self._init_providers(providers) + self._init_clients(clients) self._init_atexit() return cls._instance @@ -395,18 +399,16 @@ def _pop_tracer(self, tracer: Tracer) -> None: self._tracer.get().remove(tracer) tracer._unlink_parent(self.current_tracer) - def _init_providers(self, providers: List[str]) -> None: - for provider in providers: - if provider not in _INSTRUMENTS: - raise Scope3AIError( - f"Could not find tracer for the `{provider}` provider." - ) - if provider in self._providers and provider not in _RE_INIT_PROVIDERS: + def _init_clients(self, clients: List[str]) -> None: + for client in clients: + if client not in _INSTRUMENTS: + raise Scope3AIError(f"Could not find tracer for the `{client}` client.") + if client in self._clients and client not in _RE_INIT_CLIENTS: # already initialized continue - init_func = _INSTRUMENTS[provider] + init_func = _INSTRUMENTS[client] init_func() - self._providers.append(provider) + self._clients.append(client) def _ensure_worker(self) -> None: if not self._worker: diff --git a/scope3ai/tracers/anthropic/chat.py b/scope3ai/tracers/anthropic/chat.py index 8a201c1..81a48f8 100644 --- a/scope3ai/tracers/anthropic/chat.py +++ b/scope3ai/tracers/anthropic/chat.py @@ -18,6 +18,7 @@ from typing_extensions import override from scope3ai.api.types import Scope3AIContext, ImpactRow +from scope3ai.constants import try_provider_for_client, CLIENTS from scope3ai.lib import Scope3AI @@ -99,6 +100,7 @@ async def __stream_text__(self) -> AsyncIterator[str]: # type: ignore[misc] requests_latency = time.perf_counter() - timer_start if model_name is not None: scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.ANTHROPIC), model_id=model_name, input_tokens=input_tokens, output_tokens=output_tokens, @@ -169,6 +171,7 @@ def __stream__(self) -> Iterator[_T]: request_latency = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.ANTHROPIC), model_id=model, input_tokens=input_tokens, output_tokens=output_tokens, @@ -201,6 +204,7 @@ async def __stream__(self) -> AsyncIterator[_T]: request_latency = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.ANTHROPIC), model_id=model, input_tokens=input_tokens, output_tokens=output_tokens, @@ -219,6 +223,7 @@ def __init__(self, parent) -> None: # noqa: ANN001 def _anthropic_chat_wrapper(response: Message, request_latency: float) -> Message: model_name = response.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.ANTHROPIC), model_id=model_name, input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens, @@ -252,6 +257,7 @@ async def _anthropic_async_chat_wrapper( ) -> Message: model_name = response.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.ANTHROPIC), model_id=model_name, input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens, diff --git a/scope3ai/tracers/cohere/chat.py b/scope3ai/tracers/cohere/chat.py index 473f93d..e8f3fe2 100644 --- a/scope3ai/tracers/cohere/chat.py +++ b/scope3ai/tracers/cohere/chat.py @@ -11,6 +11,7 @@ StreamEndStreamedChatResponse as _StreamEndStreamedChatResponse, ) +from scope3ai.constants import CLIENTS, try_provider_for_client from scope3ai.lib import Scope3AI from scope3ai.api.types import Scope3AIContext, ImpactRow @@ -40,6 +41,7 @@ def cohere_chat_wrapper( request_latency = time.perf_counter() - timer_start model_name = kwargs.get("model", "command-r") scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=response.meta.tokens.input_tokens, output_tokens=response.meta.tokens.output_tokens, @@ -60,6 +62,7 @@ async def cohere_async_chat_wrapper( request_latency = time.perf_counter() - timer_start model_name = kwargs.get("model", "command-r") scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=response.meta.tokens.input_tokens, output_tokens=response.meta.tokens.output_tokens, @@ -84,6 +87,7 @@ def cohere_stream_chat_wrapper( input_tokens = event.response.meta.tokens.input_tokens output_tokens = event.response.meta.tokens.output_tokens scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=input_tokens, output_tokens=output_tokens, @@ -110,6 +114,7 @@ async def cohere_async_stream_chat_wrapper( input_tokens = event.response.meta.tokens.input_tokens output_tokens = event.response.meta.tokens.output_tokens scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=input_tokens, output_tokens=output_tokens, diff --git a/scope3ai/tracers/cohere/chat_v2.py b/scope3ai/tracers/cohere/chat_v2.py index e757db3..6c21666 100644 --- a/scope3ai/tracers/cohere/chat_v2.py +++ b/scope3ai/tracers/cohere/chat_v2.py @@ -12,6 +12,7 @@ from scope3ai.api.types import ImpactRow, Scope3AIContext from scope3ai.lib import Scope3AI +from scope3ai.constants import CLIENTS, try_provider_for_client class ChatResponse(_ChatResponse): @@ -43,6 +44,7 @@ def cohere_chat_v2_wrapper( request_latency = time.perf_counter() - timer_start model_name = kwargs["model"] scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=response.usage.tokens.input_tokens, output_tokens=response.usage.tokens.output_tokens, @@ -63,6 +65,7 @@ async def cohere_async_chat_v2_wrapper( request_latency = time.perf_counter() - timer_start model_name = kwargs["model"] scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=response.usage.tokens.input_tokens, output_tokens=response.usage.tokens.output_tokens, @@ -88,6 +91,7 @@ def cohere_stream_chat_v2_wrapper( input_tokens = event.delta.usage.tokens.input_tokens output_tokens = event.delta.usage.tokens.output_tokens scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=input_tokens, output_tokens=output_tokens, @@ -113,6 +117,7 @@ async def cohere_async_stream_chat_v2_wrapper( input_tokens = event.delta.usage.tokens.input_tokens output_tokens = event.delta.usage.tokens.output_tokens scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.COHERE), model_id=model_name, input_tokens=input_tokens, output_tokens=output_tokens, diff --git a/scope3ai/tracers/google_genai/chat.py b/scope3ai/tracers/google_genai/chat.py index 2a523be..39200c8 100644 --- a/scope3ai/tracers/google_genai/chat.py +++ b/scope3ai/tracers/google_genai/chat.py @@ -6,6 +6,7 @@ from scope3ai.api.types import Scope3AIContext from scope3ai.api.typesgen import ImpactRow from scope3ai.lib import Scope3AI +from scope3ai.constants import CLIENTS, try_provider_for_client class GenerateContentResponse(_GenerateContentResponse): @@ -14,6 +15,7 @@ class GenerateContentResponse(_GenerateContentResponse): def get_impact_row(response: _GenerateContentResponse, duration_ms: float) -> ImpactRow: return ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.GOOGLE_OPENAI), model_id=response.model_version, input_tokens=response.usage_metadata.prompt_token_count, output_tokens=response.usage_metadata.candidates_token_count or 0, diff --git a/scope3ai/tracers/huggingface/chat.py b/scope3ai/tracers/huggingface/chat.py index 02ea8cf..d4abcbc 100644 --- a/scope3ai/tracers/huggingface/chat.py +++ b/scope3ai/tracers/huggingface/chat.py @@ -15,6 +15,7 @@ from scope3ai.api.types import ImpactRow, Scope3AIContext from scope3ai.lib import Scope3AI from scope3ai.response_interceptor.requests_interceptor import requests_response_capture +from scope3ai.constants import CLIENTS, try_provider_for_client HUGGING_FACE_CHAT_TASK = "chat" @@ -58,6 +59,7 @@ def huggingface_chat_wrapper_non_stream( else: compute_time = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.HUGGINGFACE_HUB), model_id=model, input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.completion_tokens, @@ -84,6 +86,7 @@ def huggingface_chat_wrapper_stream( token_count += 1 request_latency = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.HUGGINGFACE_HUB), model_id=model, output_tokens=token_count, request_duration_ms=request_latency * 1000, @@ -119,6 +122,7 @@ async def huggingface_async_chat_wrapper_non_stream( encoder = tiktoken.get_encoding("cl100k_base") output_tokens = len(encoder.encode(response.choices[0].message.content)) scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.HUGGINGFACE_HUB), model_id=model, input_tokens=response.usage.prompt_tokens, output_tokens=output_tokens, @@ -143,6 +147,7 @@ async def huggingface_async_chat_wrapper_stream( token_count += 1 request_latency = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.HUGGINGFACE_HUB), model_id=model, output_tokens=token_count, request_duration_ms=request_latency diff --git a/scope3ai/tracers/litellm/chat.py b/scope3ai/tracers/litellm/chat.py index 542df54..a759796 100644 --- a/scope3ai/tracers/litellm/chat.py +++ b/scope3ai/tracers/litellm/chat.py @@ -9,6 +9,7 @@ from scope3ai import Scope3AI from scope3ai.api.types import Scope3AIContext, ImpactRow +from scope3ai.constants import CLIENTS, try_provider_for_client from scope3ai.tracers.utils.multimodal import ( aggregate_multimodal, aggregate_multimodal_audio_content_output, @@ -61,6 +62,7 @@ def litellm_chat_wrapper_stream( # type: ignore[misc] if model is None: model = chunk.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.LITELLM), model_id=model, input_tokens=input_tokens, output_tokens=token_count, @@ -89,6 +91,7 @@ def litellm_chat_wrapper_non_stream( if model is None: model = response.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.LITELLM), model_id=model, input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.total_tokens, @@ -141,6 +144,7 @@ async def litellm_async_chat_wrapper_base( if model is None: model = response.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.LITELLM), model_id=model, input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.total_tokens, @@ -193,6 +197,7 @@ async def litellm_async_chat_wrapper_stream( # type: ignore[misc] if model is None: model = chunk.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.LITELLM), model_id=model, input_tokens=input_tokens, output_tokens=token_count, diff --git a/scope3ai/tracers/mistralai/chat.py b/scope3ai/tracers/mistralai/chat.py index aa8935b..44d795c 100644 --- a/scope3ai/tracers/mistralai/chat.py +++ b/scope3ai/tracers/mistralai/chat.py @@ -11,6 +11,7 @@ from scope3ai import Scope3AI from scope3ai.api.types import Scope3AIContext from scope3ai.api.typesgen import ImpactRow +from scope3ai.constants import CLIENTS, try_provider_for_client from scope3ai.tracers.utils.multimodal import aggregate_multimodal @@ -35,6 +36,7 @@ def mistralai_v1_chat_wrapper( response = wrapped(*args, **kwargs) request_latency = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.MISTRALAI), model_id=response.model, input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.completion_tokens, @@ -64,6 +66,7 @@ def mistralai_v1_chat_wrapper_stream( continue request_latency = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.MISTRALAI), model_id=model_name, input_tokens=chunk.data.usage.prompt_tokens, output_tokens=chunk.data.usage.completion_tokens, @@ -84,6 +87,7 @@ async def mistralai_v1_async_chat_wrapper( response = await wrapped(*args, **kwargs) request_latency = time.perf_counter() - timer_start scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.MISTRALAI), model_id=response.model, input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.completion_tokens, @@ -105,6 +109,7 @@ async def _generator( request_latency = time.perf_counter() - timer_start model_name = chunk.data.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.MISTRALAI), model_id=model_name, input_tokens=chunk.data.usage.prompt_tokens, output_tokens=chunk.data.usage.completion_tokens, diff --git a/scope3ai/tracers/openai/chat.py b/scope3ai/tracers/openai/chat.py index 9543549..ff252e4 100644 --- a/scope3ai/tracers/openai/chat.py +++ b/scope3ai/tracers/openai/chat.py @@ -10,6 +10,7 @@ from scope3ai.api.types import ImpactRow, Scope3AIContext from scope3ai.lib import Scope3AI +from scope3ai.constants import CLIENTS, try_provider_for_client from scope3ai.tracers.utils.multimodal import ( aggregate_multimodal, aggregate_multimodal_audio_content_output, @@ -40,6 +41,7 @@ def _openai_chat_wrapper( http_response = response.http_response.json() model_used = http_response.get("model") scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.OPENAI), model_id=model_requested, model_used_id=model_used, input_tokens=http_response.get("usage", {}).get("prompt_tokens"), @@ -62,6 +64,7 @@ def _openai_chat_wrapper( return response, scope3_row else: scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.OPENAI), model_id=model_requested, model_used_id=response.model, input_tokens=response.usage.prompt_tokens, @@ -128,6 +131,7 @@ def openai_chat_wrapper_stream( model_used = chunk.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.OPENAI), model_id=model_requested, model_used_id=model_used, input_tokens=chunk.usage.prompt_tokens, @@ -184,6 +188,7 @@ async def openai_async_chat_wrapper_stream( model_used = chunk.model scope3_row = ImpactRow( + managed_service_id=try_provider_for_client(CLIENTS.OPENAI), model_id=model_requested, model_used_id=model_used, input_tokens=chunk.usage.prompt_tokens, diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 0000000..90f7dc3 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,10 @@ +import pytest +from scope3ai.constants import CLIENTS, try_provider_for_client + + +@pytest.mark.parametrize( + "client", + [client for client in CLIENTS], +) +def test_client_to_provider(client): + try_provider_for_client(client) diff --git a/tests/test_lib_metadata.py b/tests/test_lib_metadata.py index ce39750..342b497 100644 --- a/tests/test_lib_metadata.py +++ b/tests/test_lib_metadata.py @@ -8,7 +8,7 @@ def test_lib_init_default(): scope3: Optional[Scope3AI] = None try: - scope3 = Scope3AI.init(api_key="dummy", providers=[]) + scope3 = Scope3AI.init(api_key="dummy", provider_clients=[]) assert scope3.environment is None assert scope3.application_id == "default" assert scope3.client_id is None @@ -38,7 +38,7 @@ def test_lib_init_env(init_env): scope3: Optional[Scope3AI] = None try: - scope3 = Scope3AI.init(api_key="dummy", providers=[]) + scope3 = Scope3AI.init(api_key="dummy", provider_clients=[]) assert scope3.environment == "environment" assert scope3.application_id == "application_id" assert scope3.client_id == "client_id" @@ -59,7 +59,7 @@ def test_lib_init_precedence(init_env): application_id="application_id_2", client_id="client_id_2", project_id="project_id_2", - providers=[], + provider_clients=[], ) assert scope3.environment == "environment_2" assert scope3.application_id == "application_id_2" From 57288bf633d6d04e7185b63765359e3311df6e7c Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Wed, 26 Mar 2025 15:51:21 -0400 Subject: [PATCH 2/3] feat(api): msp-dictionary-update --- scope3ai/constants.py | 8 ++++---- scope3ai/lib.py | 2 +- scope3ai/tracers/google_genai/chat.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scope3ai/constants.py b/scope3ai/constants.py index 1d92493..d483358 100644 --- a/scope3ai/constants.py +++ b/scope3ai/constants.py @@ -5,7 +5,7 @@ # client is an API provided by managed service providers or third parties to interact with managed service providers class CLIENTS(Enum): # some clients hide more than 1 provider, like monsieur Google. We want to distinguish between attaching to a client and sending a provider - GOOGLE_OPENAI = "google-openai" + GOOGLE_GENAI = "google-genai" OPENAI = "openai" ANTHROPIC = "anthropic" @@ -58,8 +58,8 @@ class PROVIDERS(Enum): # API to Provider are many to many # but we assume they x = x for all the providers/clients in PROVIDER_CLIENTS PROVIDER_TO_CLIENT = { - PROVIDERS.GOOGLE_GEMINI: [CLIENTS.GOOGLE_OPENAI], - PROVIDERS.GOOGLE_VERTEX: [CLIENTS.GOOGLE_OPENAI], + PROVIDERS.GOOGLE_GEMINI: [CLIENTS.GOOGLE_GENAI], + PROVIDERS.GOOGLE_VERTEX: [CLIENTS.GOOGLE_GENAI], PROVIDERS.OPENAI: [CLIENTS.OPENAI], PROVIDERS.ANTHROPIC: [CLIENTS.ANTHROPIC], PROVIDERS.COHERE: [CLIENTS.COHERE], @@ -70,7 +70,7 @@ class PROVIDERS(Enum): } CLIENT_TO_PROVIDER = { - CLIENTS.GOOGLE_OPENAI: [PROVIDERS.GOOGLE_GEMINI, PROVIDERS.GOOGLE_VERTEX], + CLIENTS.GOOGLE_GENAI: [PROVIDERS.GOOGLE_GEMINI, PROVIDERS.GOOGLE_VERTEX], CLIENTS.OPENAI: [PROVIDERS.OPENAI], CLIENTS.ANTHROPIC: [PROVIDERS.ANTHROPIC], CLIENTS.COHERE: [PROVIDERS.COHERE], diff --git a/scope3ai/lib.py b/scope3ai/lib.py index 48d9fa5..a998d6f 100644 --- a/scope3ai/lib.py +++ b/scope3ai/lib.py @@ -88,7 +88,7 @@ def init_response_instrumentor() -> None: CLIENTS.OPENAI.value: init_openai_instrumentor, CLIENTS.HUGGINGFACE_HUB.value: init_huggingface_hub_instrumentor, # TODO current tests use gemini - CLIENTS.GOOGLE_OPENAI.value: init_google_genai_instrumentor, + CLIENTS.GOOGLE_GENAI.value: init_google_genai_instrumentor, CLIENTS.LITELLM.value: init_litellm_instrumentor, CLIENTS.MISTRALAI.value: init_mistral_v1_instrumentor, CLIENTS.RESPONSE.value: init_response_instrumentor, diff --git a/scope3ai/tracers/google_genai/chat.py b/scope3ai/tracers/google_genai/chat.py index 39200c8..7f0534b 100644 --- a/scope3ai/tracers/google_genai/chat.py +++ b/scope3ai/tracers/google_genai/chat.py @@ -15,7 +15,7 @@ class GenerateContentResponse(_GenerateContentResponse): def get_impact_row(response: _GenerateContentResponse, duration_ms: float) -> ImpactRow: return ImpactRow( - managed_service_id=try_provider_for_client(CLIENTS.GOOGLE_OPENAI), + managed_service_id=try_provider_for_client(CLIENTS.GOOGLE_GENAI), model_id=response.model_version, input_tokens=response.usage_metadata.prompt_token_count, output_tokens=response.usage_metadata.candidates_token_count or 0, From 5758a3b9b5f9faf8bc5e4d9c155d9b10d1d8e19c Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Wed, 26 Mar 2025 21:41:45 -0400 Subject: [PATCH 3/3] feat(api): client-to-provider dry --- scope3ai/constants.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/scope3ai/constants.py b/scope3ai/constants.py index d483358..11d212c 100644 --- a/scope3ai/constants.py +++ b/scope3ai/constants.py @@ -69,16 +69,12 @@ class PROVIDERS(Enum): PROVIDERS.RESPONSE: [CLIENTS.RESPONSE], } -CLIENT_TO_PROVIDER = { - CLIENTS.GOOGLE_GENAI: [PROVIDERS.GOOGLE_GEMINI, PROVIDERS.GOOGLE_VERTEX], - CLIENTS.OPENAI: [PROVIDERS.OPENAI], - CLIENTS.ANTHROPIC: [PROVIDERS.ANTHROPIC], - CLIENTS.COHERE: [PROVIDERS.COHERE], - CLIENTS.HUGGINGFACE_HUB: [PROVIDERS.HUGGINGFACE_HUB], - CLIENTS.LITELLM: [PROVIDERS.LITELLM], - CLIENTS.MISTRALAI: [PROVIDERS.MISTRALAI], - CLIENTS.RESPONSE: [PROVIDERS.RESPONSE], -} +CLIENT_TO_PROVIDER = {} +for k, v in PROVIDER_TO_CLIENT.items(): + for client in v: + if client not in CLIENT_TO_PROVIDER: + CLIENT_TO_PROVIDER[client] = [] + CLIENT_TO_PROVIDER[client].append(k) def try_provider_for_client(client: CLIENTS) -> Optional[PROVIDERS]: