magic-lab-observer 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- magic_lab_observer-0.1.1/.gitignore +68 -0
- magic_lab_observer-0.1.1/PKG-INFO +75 -0
- magic_lab_observer-0.1.1/README.md +39 -0
- magic_lab_observer-0.1.1/pyproject.toml +63 -0
- magic_lab_observer-0.1.1/src/llm_observatory/__init__.py +32 -0
- magic_lab_observer-0.1.1/src/llm_observatory/exporters/__init__.py +23 -0
- magic_lab_observer-0.1.1/src/llm_observatory/exporters/otlp.py +90 -0
- magic_lab_observer-0.1.1/src/llm_observatory/instrumentors/__init__.py +57 -0
- magic_lab_observer-0.1.1/src/llm_observatory/instrumentors/anthropic.py +135 -0
- magic_lab_observer-0.1.1/src/llm_observatory/instrumentors/langchain.py +156 -0
- magic_lab_observer-0.1.1/src/llm_observatory/instrumentors/openai.py +137 -0
- magic_lab_observer-0.1.1/src/llm_observatory/pricing.py +78 -0
- magic_lab_observer-0.1.1/src/llm_observatory/tracer.py +379 -0
- magic_lab_observer-0.1.1/tests/__init__.py +0 -0
- magic_lab_observer-0.1.1/tests/test_instrumentors.py +227 -0
- magic_lab_observer-0.1.1/tests/test_tracer.py +190 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.vscode/
|
|
30
|
+
.idea/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
*~
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
.coverage
|
|
37
|
+
htmlcov/
|
|
38
|
+
.pytest_cache/
|
|
39
|
+
.mypy_cache/
|
|
40
|
+
|
|
41
|
+
# Node.js
|
|
42
|
+
node_modules/
|
|
43
|
+
npm-debug.log*
|
|
44
|
+
yarn-debug.log*
|
|
45
|
+
yarn-error.log*
|
|
46
|
+
|
|
47
|
+
# Build
|
|
48
|
+
dist/
|
|
49
|
+
build/
|
|
50
|
+
*.tsbuildinfo
|
|
51
|
+
|
|
52
|
+
# Environment
|
|
53
|
+
.env
|
|
54
|
+
.env.local
|
|
55
|
+
.env.*.local
|
|
56
|
+
|
|
57
|
+
# Podman
|
|
58
|
+
podman-compose.override.yml
|
|
59
|
+
|
|
60
|
+
# Database
|
|
61
|
+
*.db
|
|
62
|
+
*.sqlite3
|
|
63
|
+
.observer-state/
|
|
64
|
+
|
|
65
|
+
# OS
|
|
66
|
+
.DS_Store
|
|
67
|
+
Thumbs.db
|
|
68
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: magic-lab-observer
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for LLM Observatory - Open-source LLM observability
|
|
5
|
+
Project-URL: Homepage, https://github.com/Magic-Lab-Studio/observer
|
|
6
|
+
Project-URL: Repository, https://github.com/Magic-Lab-Studio/observer.git
|
|
7
|
+
Project-URL: Issues, https://github.com/Magic-Lab-Studio/observer/issues
|
|
8
|
+
Author: LLM Observatory Contributors
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: anthropic,llm,observability,openai,tracing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Provides-Extra: all
|
|
21
|
+
Requires-Dist: anthropic>=0.20.0; extra == 'all'
|
|
22
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'all'
|
|
23
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
24
|
+
Provides-Extra: anthropic
|
|
25
|
+
Requires-Dist: anthropic>=0.20.0; extra == 'anthropic'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
31
|
+
Provides-Extra: langchain
|
|
32
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
|
|
33
|
+
Provides-Extra: openai
|
|
34
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# LLM Observatory Python SDK
|
|
38
|
+
|
|
39
|
+
Open-source LLM observability SDK for tracing and monitoring AI applications.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install magic-lab-observer
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
With optional integrations:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install magic-lab-observer[openai,anthropic,langchain]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
The distribution name is `magic-lab-observer`; the stable Python import remains
|
|
56
|
+
`llm_observatory`.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from llm_observatory import instrument, trace
|
|
60
|
+
|
|
61
|
+
# Auto-instrument LLM libraries
|
|
62
|
+
instrument(openai=True, anthropic=True)
|
|
63
|
+
|
|
64
|
+
# Or manually trace functions
|
|
65
|
+
@trace(name="summarize")
|
|
66
|
+
def summarize(text: str):
|
|
67
|
+
return openai.chat.completions.create(
|
|
68
|
+
model="gpt-4",
|
|
69
|
+
messages=[{"role": "user", "content": text}]
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Apache License 2.0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# LLM Observatory Python SDK
|
|
2
|
+
|
|
3
|
+
Open-source LLM observability SDK for tracing and monitoring AI applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install magic-lab-observer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
With optional integrations:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install magic-lab-observer[openai,anthropic,langchain]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
The distribution name is `magic-lab-observer`; the stable Python import remains
|
|
20
|
+
`llm_observatory`.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from llm_observatory import instrument, trace
|
|
24
|
+
|
|
25
|
+
# Auto-instrument LLM libraries
|
|
26
|
+
instrument(openai=True, anthropic=True)
|
|
27
|
+
|
|
28
|
+
# Or manually trace functions
|
|
29
|
+
@trace(name="summarize")
|
|
30
|
+
def summarize(text: str):
|
|
31
|
+
return openai.chat.completions.create(
|
|
32
|
+
model="gpt-4",
|
|
33
|
+
messages=[{"role": "user", "content": text}]
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
Apache License 2.0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "magic-lab-observer"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Python SDK for LLM Observatory - Open-source LLM observability"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "LLM Observatory Contributors" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["llm", "observability", "tracing", "openai", "anthropic"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Magic-Lab-Studio/observer"
|
|
32
|
+
Repository = "https://github.com/Magic-Lab-Studio/observer.git"
|
|
33
|
+
Issues = "https://github.com/Magic-Lab-Studio/observer/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
openai = ["openai>=1.0.0"]
|
|
37
|
+
anthropic = ["anthropic>=0.20.0"]
|
|
38
|
+
langchain = ["langchain-core>=0.2.0"]
|
|
39
|
+
all = [
|
|
40
|
+
"openai>=1.0.0",
|
|
41
|
+
"anthropic>=0.20.0",
|
|
42
|
+
"langchain-core>=0.2.0",
|
|
43
|
+
]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=8.0",
|
|
46
|
+
"pytest-cov>=5.0",
|
|
47
|
+
"ruff>=0.5",
|
|
48
|
+
"mypy>=1.10",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/llm_observatory"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = "py310"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = "3.10"
|
|
63
|
+
strict = true
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""LLM Observatory Python SDK - Open-source LLM observability."""
|
|
2
|
+
|
|
3
|
+
from llm_observatory.exporters.otlp import OTLPExporter
|
|
4
|
+
from llm_observatory.instrumentors import instrument, uninstrument
|
|
5
|
+
from llm_observatory.pricing import MODEL_PRICING, calculate_cost, get_model_pricing
|
|
6
|
+
from llm_observatory.tracer import (
|
|
7
|
+
Span,
|
|
8
|
+
SpanStatus,
|
|
9
|
+
TokenUsage,
|
|
10
|
+
Tracer,
|
|
11
|
+
get_tracer,
|
|
12
|
+
set_tracer,
|
|
13
|
+
trace,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.1"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Tracer",
|
|
20
|
+
"Span",
|
|
21
|
+
"SpanStatus",
|
|
22
|
+
"TokenUsage",
|
|
23
|
+
"trace",
|
|
24
|
+
"get_tracer",
|
|
25
|
+
"set_tracer",
|
|
26
|
+
"instrument",
|
|
27
|
+
"uninstrument",
|
|
28
|
+
"calculate_cost",
|
|
29
|
+
"get_model_pricing",
|
|
30
|
+
"MODEL_PRICING",
|
|
31
|
+
"OTLPExporter",
|
|
32
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Exporters for sending traces to backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseExporter(ABC):
|
|
10
|
+
"""Base class for trace exporters."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def export(self, spans: list[dict[str, Any]]) -> None:
|
|
14
|
+
"""Export spans to the backend.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
spans: List of span dictionaries to export.
|
|
18
|
+
"""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def flush(self) -> None:
|
|
22
|
+
"""Flush any buffered spans."""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""OTLP HTTP exporter for sending traces to observatory backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from llm_observatory.exporters import BaseExporter
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OTLPExporter(BaseExporter):
|
|
16
|
+
"""Exports traces to an OTLP-compatible backend via HTTP."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
endpoint: str = "http://localhost:8000",
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
timeout: float = 10.0,
|
|
23
|
+
max_retries: int = 3,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initialize the exporter.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
endpoint: Backend endpoint URL.
|
|
29
|
+
api_key: Optional API key for authentication.
|
|
30
|
+
timeout: Request timeout in seconds.
|
|
31
|
+
max_retries: Maximum number of retries on failure.
|
|
32
|
+
"""
|
|
33
|
+
self.endpoint = endpoint.rstrip("/")
|
|
34
|
+
self.api_key = api_key
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self.max_retries = max_retries
|
|
37
|
+
self._client = httpx.Client(timeout=timeout)
|
|
38
|
+
|
|
39
|
+
def export(self, spans: list[dict[str, Any]]) -> None:
|
|
40
|
+
"""Export spans to the backend.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
spans: List of span dictionaries to export.
|
|
44
|
+
"""
|
|
45
|
+
headers = {"Content-Type": "application/json"}
|
|
46
|
+
if self.api_key:
|
|
47
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
48
|
+
|
|
49
|
+
for attempt in range(self.max_retries):
|
|
50
|
+
try:
|
|
51
|
+
response = self._client.post(
|
|
52
|
+
f"{self.endpoint}/v1/traces/batch",
|
|
53
|
+
json={"spans": spans},
|
|
54
|
+
headers=headers,
|
|
55
|
+
)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
logger.debug(f"Exported {len(spans)} spans successfully")
|
|
58
|
+
return
|
|
59
|
+
except httpx.HTTPStatusError as e:
|
|
60
|
+
logger.warning(
|
|
61
|
+
f"Export failed (attempt {attempt + 1}/{self.max_retries}): "
|
|
62
|
+
f"HTTP {e.response.status_code} - {e.response.text}"
|
|
63
|
+
)
|
|
64
|
+
if attempt == self.max_retries - 1:
|
|
65
|
+
msg = (
|
|
66
|
+
f"Export failed after {self.max_retries} attempts, "
|
|
67
|
+
f"dropping {len(spans)} spans"
|
|
68
|
+
)
|
|
69
|
+
logger.error(msg)
|
|
70
|
+
except httpx.HTTPError as e:
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"Export failed (attempt {attempt + 1}/{self.max_retries}): {e}"
|
|
73
|
+
)
|
|
74
|
+
if attempt == self.max_retries - 1:
|
|
75
|
+
msg = (
|
|
76
|
+
f"Export failed after {self.max_retries} attempts, "
|
|
77
|
+
f"dropping {len(spans)} spans"
|
|
78
|
+
)
|
|
79
|
+
logger.error(msg)
|
|
80
|
+
|
|
81
|
+
def flush(self) -> None:
|
|
82
|
+
"""Flush any buffered spans."""
|
|
83
|
+
self._client.close()
|
|
84
|
+
|
|
85
|
+
def __del__(self) -> None:
|
|
86
|
+
"""Clean up the client."""
|
|
87
|
+
try:
|
|
88
|
+
self._client.close()
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Auto-instrumentation for LLM libraries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
# Store instrumentor instances for uninstrument()
|
|
8
|
+
_active_instrumentors: list[Any] = []
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def instrument(
|
|
12
|
+
openai: bool = False,
|
|
13
|
+
anthropic: bool = False,
|
|
14
|
+
langchain: bool = False,
|
|
15
|
+
**kwargs: Any,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Auto-instrument LLM libraries.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
openai: Enable OpenAI instrumentation.
|
|
21
|
+
anthropic: Enable Anthropic instrumentation.
|
|
22
|
+
langchain: Enable LangChain instrumentation.
|
|
23
|
+
**kwargs: Additional configuration options.
|
|
24
|
+
"""
|
|
25
|
+
if openai:
|
|
26
|
+
from llm_observatory.instrumentors.openai import OpenAIInstrumentor
|
|
27
|
+
|
|
28
|
+
inst = OpenAIInstrumentor()
|
|
29
|
+
inst.instrument()
|
|
30
|
+
_active_instrumentors.append(inst)
|
|
31
|
+
|
|
32
|
+
if anthropic:
|
|
33
|
+
from llm_observatory.instrumentors.anthropic import AnthropicInstrumentor
|
|
34
|
+
|
|
35
|
+
inst = AnthropicInstrumentor()
|
|
36
|
+
inst.instrument()
|
|
37
|
+
_active_instrumentors.append(inst)
|
|
38
|
+
|
|
39
|
+
if langchain:
|
|
40
|
+
try:
|
|
41
|
+
from llm_observatory.instrumentors.langchain import LangChainInstrumentor
|
|
42
|
+
|
|
43
|
+
inst = LangChainInstrumentor()
|
|
44
|
+
inst.instrument()
|
|
45
|
+
_active_instrumentors.append(inst)
|
|
46
|
+
except ImportError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def uninstrument() -> None:
|
|
51
|
+
"""Uninstrument all active instrumentors."""
|
|
52
|
+
for inst in _active_instrumentors:
|
|
53
|
+
try:
|
|
54
|
+
inst.uninstrument()
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
_active_instrumentors.clear()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Anthropic auto-instrumentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from llm_observatory.pricing import calculate_cost
|
|
8
|
+
from llm_observatory.tracer import Span, SpanStatus, get_tracer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnthropicInstrumentor:
|
|
12
|
+
"""Instruments Anthropic API calls to capture traces."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._original_sync_create: Any | None = None
|
|
16
|
+
self._original_async_create: Any | None = None
|
|
17
|
+
|
|
18
|
+
def instrument(self) -> None:
|
|
19
|
+
"""Patch Anthropic to capture traces."""
|
|
20
|
+
try:
|
|
21
|
+
import anthropic
|
|
22
|
+
|
|
23
|
+
# Patch sync messages
|
|
24
|
+
self._original_sync_create = anthropic.resources.messages.Messages.create
|
|
25
|
+
anthropic.resources.messages.Messages.create = self._patched_sync_create
|
|
26
|
+
|
|
27
|
+
# Patch async messages
|
|
28
|
+
self._original_async_create = (
|
|
29
|
+
anthropic.resources.messages.AsyncMessages.create
|
|
30
|
+
)
|
|
31
|
+
anthropic.resources.messages.AsyncMessages.create = (
|
|
32
|
+
self._patched_async_create
|
|
33
|
+
)
|
|
34
|
+
except ImportError:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def _build_span(
|
|
38
|
+
self, model: str, messages: list[dict[str, Any]]
|
|
39
|
+
) -> tuple[Span, Any]:
|
|
40
|
+
"""Build a span for an Anthropic call."""
|
|
41
|
+
tracer = get_tracer()
|
|
42
|
+
span = tracer.start_span(
|
|
43
|
+
name="anthropic.messages.create",
|
|
44
|
+
span_type="llm",
|
|
45
|
+
)
|
|
46
|
+
span.set_input({"model": model, "messages": messages})
|
|
47
|
+
span.set_attribute("gen_ai.system", "anthropic")
|
|
48
|
+
span.set_attribute("gen_ai.request.model", model)
|
|
49
|
+
return span, tracer
|
|
50
|
+
|
|
51
|
+
def _finish_span(self, span: Span, result: Any, model: str) -> None:
|
|
52
|
+
"""Extract data from result and finish span."""
|
|
53
|
+
try:
|
|
54
|
+
# Extract token usage
|
|
55
|
+
if hasattr(result, "usage"):
|
|
56
|
+
input_tokens = getattr(result.usage, "input_tokens", 0) or 0
|
|
57
|
+
output_tokens = getattr(result.usage, "output_tokens", 0) or 0
|
|
58
|
+
span.set_token_usage(input_tokens, output_tokens)
|
|
59
|
+
|
|
60
|
+
# Calculate cost
|
|
61
|
+
cost = calculate_cost(model, input_tokens, output_tokens)
|
|
62
|
+
if cost is not None:
|
|
63
|
+
span.set_cost(cost)
|
|
64
|
+
|
|
65
|
+
# Extract output
|
|
66
|
+
if hasattr(result, "content") and result.content:
|
|
67
|
+
content_blocks = []
|
|
68
|
+
for block in result.content:
|
|
69
|
+
block_data = {}
|
|
70
|
+
if hasattr(block, "type"):
|
|
71
|
+
block_data["type"] = block.type
|
|
72
|
+
if hasattr(block, "text"):
|
|
73
|
+
block_data["text"] = block.text
|
|
74
|
+
content_blocks.append(block_data)
|
|
75
|
+
span.set_output({"content": content_blocks})
|
|
76
|
+
|
|
77
|
+
span.set_attribute("response_id", getattr(result, "id", None))
|
|
78
|
+
span.set_status(SpanStatus.OK)
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
span.set_status(SpanStatus.ERROR)
|
|
82
|
+
span.set_attribute("error.message", str(e))
|
|
83
|
+
|
|
84
|
+
finally:
|
|
85
|
+
span.end()
|
|
86
|
+
|
|
87
|
+
def _patched_sync_create(self, *args: Any, **kwargs: Any) -> Any:
|
|
88
|
+
"""Patched sync create method."""
|
|
89
|
+
model = kwargs.get("model", "unknown")
|
|
90
|
+
messages = kwargs.get("messages", [])
|
|
91
|
+
|
|
92
|
+
span, _ = self._build_span(model, messages)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
result = self._original_sync_create(*args, **kwargs)
|
|
96
|
+
self._finish_span(span, result, model)
|
|
97
|
+
return result
|
|
98
|
+
except Exception as e:
|
|
99
|
+
span.set_status(SpanStatus.ERROR)
|
|
100
|
+
span.set_attribute("error.message", str(e))
|
|
101
|
+
span.end()
|
|
102
|
+
raise
|
|
103
|
+
|
|
104
|
+
async def _patched_async_create(self, *args: Any, **kwargs: Any) -> Any:
|
|
105
|
+
"""Patched async create method."""
|
|
106
|
+
model = kwargs.get("model", "unknown")
|
|
107
|
+
messages = kwargs.get("messages", [])
|
|
108
|
+
|
|
109
|
+
span, _ = self._build_span(model, messages)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
result = await self._original_async_create(*args, **kwargs)
|
|
113
|
+
self._finish_span(span, result, model)
|
|
114
|
+
return result
|
|
115
|
+
except Exception as e:
|
|
116
|
+
span.set_status(SpanStatus.ERROR)
|
|
117
|
+
span.set_attribute("error.message", str(e))
|
|
118
|
+
span.end()
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
def uninstrument(self) -> None:
|
|
122
|
+
"""Remove instrumentation."""
|
|
123
|
+
try:
|
|
124
|
+
import anthropic
|
|
125
|
+
|
|
126
|
+
if self._original_sync_create is not None:
|
|
127
|
+
anthropic.resources.messages.Messages.create = (
|
|
128
|
+
self._original_sync_create
|
|
129
|
+
)
|
|
130
|
+
if self._original_async_create is not None:
|
|
131
|
+
anthropic.resources.messages.AsyncMessages.create = (
|
|
132
|
+
self._original_async_create
|
|
133
|
+
)
|
|
134
|
+
except ImportError:
|
|
135
|
+
pass
|