Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
)
```

Expand Down
1 change: 1 addition & 0 deletions scope3ai/api/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions scope3ai/base_tracer.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
88 changes: 82 additions & 6 deletions scope3ai/constants.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,88 @@
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_GENAI = "google-genai"

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_GENAI],
PROVIDERS.GOOGLE_VERTEX: [CLIENTS.GOOGLE_GENAI],
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 = {}
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]:
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]
54 changes: 28 additions & 26 deletions scope3ai/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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_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,
}

_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:
Expand All @@ -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):
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions scope3ai/tracers/anthropic/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions scope3ai/tracers/cohere/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions scope3ai/tracers/cohere/chat_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions scope3ai/tracers/google_genai/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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_GENAI),
model_id=response.model_version,
input_tokens=response.usage_metadata.prompt_token_count,
output_tokens=response.usage_metadata.candidates_token_count or 0,
Expand Down
Loading