blitz-sdk 0.1.0__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.
- blitz_sdk-0.1.0/.github/workflows/publish.yml +30 -0
- blitz_sdk-0.1.0/.gitignore +10 -0
- blitz_sdk-0.1.0/PKG-INFO +22 -0
- blitz_sdk-0.1.0/README.md +3 -0
- blitz_sdk-0.1.0/blitz/__init__.py +121 -0
- blitz_sdk-0.1.0/blitz/_exporter.py +182 -0
- blitz_sdk-0.1.0/blitz/_instrument.py +52 -0
- blitz_sdk-0.1.0/examples/quickstart.py +33 -0
- blitz_sdk-0.1.0/pyproject.toml +30 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*" # e.g. v0.1.0
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write # PyPI Trusted Publishing (OIDC)
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install build tools
|
|
24
|
+
run: pip install build
|
|
25
|
+
|
|
26
|
+
- name: Build sdist and wheel
|
|
27
|
+
run: python -m build
|
|
28
|
+
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
blitz_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blitz-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in OpenTelemetry tracing for OpenAI, Anthropic, and Gemini LLM calls. Ships full I/O, token usage, model, latency, and cost to your blitz backend.
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: opentelemetry-api>=1.27.0
|
|
7
|
+
Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
8
|
+
Provides-Extra: all
|
|
9
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.33.0; extra == 'all'
|
|
10
|
+
Requires-Dist: opentelemetry-instrumentation-google-generativeai>=0.33.0; extra == 'all'
|
|
11
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.33.0; extra == 'all'
|
|
12
|
+
Provides-Extra: anthropic
|
|
13
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.33.0; extra == 'anthropic'
|
|
14
|
+
Provides-Extra: gemini
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-google-generativeai>=0.33.0; extra == 'gemini'
|
|
16
|
+
Provides-Extra: openai
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.33.0; extra == 'openai'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# blitz
|
|
21
|
+
|
|
22
|
+
Documentation coming.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""blitz — drop-in distributed tracing for LLM calls.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
import blitz
|
|
6
|
+
|
|
7
|
+
blitz.init(
|
|
8
|
+
project_id="proj_abc",
|
|
9
|
+
api_key="sk_...",
|
|
10
|
+
endpoint="https://api.sparepartslabs.com",
|
|
11
|
+
sample_rate=0.1,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
After init(), any OpenAI / Anthropic / Gemini call made in this process is traced
|
|
15
|
+
and shipped to the blitz backend. Nothing else in your code changes.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import atexit
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
from typing import Callable, Optional
|
|
24
|
+
|
|
25
|
+
from opentelemetry import trace
|
|
26
|
+
from opentelemetry.sdk.resources import Resource
|
|
27
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
28
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
29
|
+
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
|
|
30
|
+
|
|
31
|
+
from ._exporter import BlitzSpanExporter
|
|
32
|
+
from ._instrument import instrument_providers
|
|
33
|
+
|
|
34
|
+
__all__ = ["init"]
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("blitz")
|
|
37
|
+
|
|
38
|
+
_initialized = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def init(
|
|
42
|
+
*,
|
|
43
|
+
project_id: str,
|
|
44
|
+
api_key: str,
|
|
45
|
+
endpoint: str,
|
|
46
|
+
sample_rate: float = 1.0,
|
|
47
|
+
capture_content: bool = True,
|
|
48
|
+
redact: Optional[Callable[[str], str]] = None,
|
|
49
|
+
max_content_chars: int = 24_000,
|
|
50
|
+
service_name: str = "llm-app",
|
|
51
|
+
) -> list[str]:
|
|
52
|
+
"""Initialize blitz tracing.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
project_id: Your blitz project id (multitenancy key).
|
|
56
|
+
api_key: Project API key, sent as the ``x-api-key`` header.
|
|
57
|
+
endpoint: Base URL of the blitz backend, e.g.
|
|
58
|
+
``https://api.sparepartslabs.com``. The SDK posts to
|
|
59
|
+
``{endpoint}/blitz/v1/traces``.
|
|
60
|
+
sample_rate: Head sampling ratio in [0.0, 1.0]. 0.1 = trace 10% of
|
|
61
|
+
requests. Sampling is parent-based so a sampled trace keeps all its
|
|
62
|
+
child spans.
|
|
63
|
+
capture_content: When False, prompts/completions are stripped before
|
|
64
|
+
export — only metadata (model, tokens, latency, cost) is sent.
|
|
65
|
+
redact: Optional callable applied to every prompt/completion string
|
|
66
|
+
before export (PII scrubbing).
|
|
67
|
+
max_content_chars: Hard cap per content field; longer values are
|
|
68
|
+
truncated with a ``…[truncated]`` marker.
|
|
69
|
+
service_name: Logical service name attached to every span.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The list of providers that were successfully instrumented
|
|
73
|
+
(e.g. ``["openai", "anthropic"]``).
|
|
74
|
+
"""
|
|
75
|
+
global _initialized
|
|
76
|
+
if _initialized:
|
|
77
|
+
logger.warning("blitz.init() called more than once; ignoring")
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
if not 0.0 <= sample_rate <= 1.0:
|
|
81
|
+
raise ValueError("sample_rate must be between 0.0 and 1.0")
|
|
82
|
+
|
|
83
|
+
# Tell the underlying instrumentors not to capture prompt content at the
|
|
84
|
+
# source when the caller opted out — cheaper and avoids the content ever
|
|
85
|
+
# entering a span. The exporter enforces this again as a backstop.
|
|
86
|
+
if not capture_content:
|
|
87
|
+
os.environ.setdefault("TRACELOOP_TRACE_CONTENT", "false")
|
|
88
|
+
|
|
89
|
+
resource = Resource.create(
|
|
90
|
+
{
|
|
91
|
+
"service.name": service_name,
|
|
92
|
+
"blitz.project_id": project_id,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
provider = TracerProvider(
|
|
96
|
+
resource=resource,
|
|
97
|
+
sampler=ParentBased(TraceIdRatioBased(sample_rate)),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
exporter = BlitzSpanExporter(
|
|
101
|
+
endpoint=endpoint,
|
|
102
|
+
api_key=api_key,
|
|
103
|
+
project_id=project_id,
|
|
104
|
+
capture_content=capture_content,
|
|
105
|
+
redact=redact,
|
|
106
|
+
max_content_chars=max_content_chars,
|
|
107
|
+
)
|
|
108
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
109
|
+
trace.set_tracer_provider(provider)
|
|
110
|
+
|
|
111
|
+
instrumented = instrument_providers(provider)
|
|
112
|
+
atexit.register(provider.shutdown)
|
|
113
|
+
|
|
114
|
+
_initialized = True
|
|
115
|
+
logger.info(
|
|
116
|
+
"blitz initialized — project=%s providers=[%s] sample_rate=%s",
|
|
117
|
+
project_id,
|
|
118
|
+
", ".join(instrumented) or "none",
|
|
119
|
+
sample_rate,
|
|
120
|
+
)
|
|
121
|
+
return instrumented
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Span exporter that converts OTel GenAI spans into blitz's wire format and
|
|
2
|
+
POSTs them to the blitz backend.
|
|
3
|
+
|
|
4
|
+
We use our own JSON shape (not raw OTLP) because we own both ends — it keeps the
|
|
5
|
+
FastAPI ingest endpoint trivial and lets redaction happen cleanly during the
|
|
6
|
+
conversion step rather than mutating immutable OTel spans. The instrumentors
|
|
7
|
+
still emit standard OTel spans, so a customer can additionally attach a vanilla
|
|
8
|
+
OTLP exporter to fan telemetry out to Datadog/Phoenix/etc.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import urllib.request
|
|
16
|
+
from typing import Callable, Optional, Sequence
|
|
17
|
+
|
|
18
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
19
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
20
|
+
from opentelemetry.trace import StatusCode
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("blitz")
|
|
23
|
+
|
|
24
|
+
_PROMPT_PREFIX = "gen_ai.prompt."
|
|
25
|
+
_COMPLETION_PREFIX = "gen_ai.completion."
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BlitzSpanExporter(SpanExporter):
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
endpoint: str,
|
|
33
|
+
api_key: str,
|
|
34
|
+
project_id: str,
|
|
35
|
+
capture_content: bool = True,
|
|
36
|
+
redact: Optional[Callable[[str], str]] = None,
|
|
37
|
+
max_content_chars: int = 24_000,
|
|
38
|
+
timeout: float = 10.0,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._url = endpoint.rstrip("/") + "/blitz/v1/traces"
|
|
41
|
+
self._headers = {"content-type": "application/json", "x-api-key": api_key}
|
|
42
|
+
self._project_id = project_id
|
|
43
|
+
self._capture_content = capture_content
|
|
44
|
+
self._redact = redact
|
|
45
|
+
self._max = max_content_chars
|
|
46
|
+
self._timeout = timeout
|
|
47
|
+
|
|
48
|
+
# -- SpanExporter interface ---------------------------------------------
|
|
49
|
+
|
|
50
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
51
|
+
try:
|
|
52
|
+
payload = {
|
|
53
|
+
"project_id": self._project_id,
|
|
54
|
+
"spans": [self._convert(s) for s in spans],
|
|
55
|
+
}
|
|
56
|
+
data = json.dumps(payload).encode("utf-8")
|
|
57
|
+
req = urllib.request.Request(
|
|
58
|
+
self._url, data=data, headers=self._headers, method="POST"
|
|
59
|
+
)
|
|
60
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
61
|
+
if resp.status >= 300:
|
|
62
|
+
logger.warning("blitz export got HTTP %s", resp.status)
|
|
63
|
+
return SpanExportResult.FAILURE
|
|
64
|
+
return SpanExportResult.SUCCESS
|
|
65
|
+
except Exception: # noqa: BLE001 - exporting must never raise into the app
|
|
66
|
+
logger.warning("blitz export failed", exc_info=True)
|
|
67
|
+
return SpanExportResult.FAILURE
|
|
68
|
+
|
|
69
|
+
def shutdown(self) -> None: # pragma: no cover - nothing to clean up
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# -- conversion ----------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def _convert(self, span: ReadableSpan) -> dict:
|
|
75
|
+
attrs = dict(span.attributes or {})
|
|
76
|
+
prompt, completion, other = self._split_content(attrs)
|
|
77
|
+
|
|
78
|
+
content = None
|
|
79
|
+
if self._capture_content:
|
|
80
|
+
content = self._redact_content(
|
|
81
|
+
{"prompt": prompt, "completion": completion}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
ctx = span.get_span_context()
|
|
85
|
+
status = (
|
|
86
|
+
"error"
|
|
87
|
+
if span.status is not None and span.status.status_code == StatusCode.ERROR
|
|
88
|
+
else "ok"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"trace_id": format(ctx.trace_id, "032x"),
|
|
93
|
+
"span_id": format(ctx.span_id, "016x"),
|
|
94
|
+
"parent_span_id": (
|
|
95
|
+
format(span.parent.span_id, "016x") if span.parent else None
|
|
96
|
+
),
|
|
97
|
+
"name": span.name,
|
|
98
|
+
"provider": attrs.get("gen_ai.system"),
|
|
99
|
+
"model": attrs.get("gen_ai.response.model")
|
|
100
|
+
or attrs.get("gen_ai.request.model"),
|
|
101
|
+
"input_tokens": _first_int(
|
|
102
|
+
attrs,
|
|
103
|
+
"gen_ai.usage.input_tokens",
|
|
104
|
+
"gen_ai.usage.prompt_tokens",
|
|
105
|
+
"llm.usage.prompt_tokens",
|
|
106
|
+
),
|
|
107
|
+
"output_tokens": _first_int(
|
|
108
|
+
attrs,
|
|
109
|
+
"gen_ai.usage.output_tokens",
|
|
110
|
+
"gen_ai.usage.completion_tokens",
|
|
111
|
+
"llm.usage.completion_tokens",
|
|
112
|
+
),
|
|
113
|
+
"start_unix_ns": span.start_time,
|
|
114
|
+
"end_unix_ns": span.end_time,
|
|
115
|
+
"status": status,
|
|
116
|
+
"attributes": _jsonable(other),
|
|
117
|
+
"content": content,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def _split_content(self, attrs: dict):
|
|
121
|
+
"""Pull the indexed prompt/completion attributes
|
|
122
|
+
(gen_ai.prompt.0.role, gen_ai.prompt.0.content, ...) into ordered lists,
|
|
123
|
+
leaving everything else in `other`."""
|
|
124
|
+
prompts: dict[str, dict] = {}
|
|
125
|
+
completions: dict[str, dict] = {}
|
|
126
|
+
other: dict = {}
|
|
127
|
+
|
|
128
|
+
for key, value in attrs.items():
|
|
129
|
+
if key.startswith(_PROMPT_PREFIX):
|
|
130
|
+
idx, _, field = key[len(_PROMPT_PREFIX) :].partition(".")
|
|
131
|
+
prompts.setdefault(idx, {})[field or "value"] = value
|
|
132
|
+
elif key.startswith(_COMPLETION_PREFIX):
|
|
133
|
+
idx, _, field = key[len(_COMPLETION_PREFIX) :].partition(".")
|
|
134
|
+
completions.setdefault(idx, {})[field or "value"] = value
|
|
135
|
+
else:
|
|
136
|
+
other[key] = value
|
|
137
|
+
|
|
138
|
+
return _ordered(prompts), _ordered(completions), other
|
|
139
|
+
|
|
140
|
+
def _redact_content(self, content: dict) -> dict:
|
|
141
|
+
for bucket in ("prompt", "completion"):
|
|
142
|
+
for msg in content.get(bucket, []):
|
|
143
|
+
if "content" in msg and isinstance(msg["content"], str):
|
|
144
|
+
msg["content"] = self._scrub(msg["content"])
|
|
145
|
+
return content
|
|
146
|
+
|
|
147
|
+
def _scrub(self, text: str) -> str:
|
|
148
|
+
if self._redact:
|
|
149
|
+
try:
|
|
150
|
+
text = self._redact(text)
|
|
151
|
+
except Exception: # noqa: BLE001
|
|
152
|
+
logger.warning("blitz redact callable raised", exc_info=True)
|
|
153
|
+
if self._max and len(text) > self._max:
|
|
154
|
+
text = text[: self._max] + "…[truncated]"
|
|
155
|
+
return text
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _ordered(indexed: dict[str, dict]) -> list[dict]:
|
|
159
|
+
return [
|
|
160
|
+
indexed[i]
|
|
161
|
+
for i in sorted(indexed, key=lambda x: int(x) if x.isdigit() else 0)
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _first_int(attrs: dict, *keys: str):
|
|
166
|
+
for key in keys:
|
|
167
|
+
if key in attrs and attrs[key] is not None:
|
|
168
|
+
try:
|
|
169
|
+
return int(attrs[key])
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
continue
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _jsonable(obj):
|
|
176
|
+
"""OTel attribute values are already JSON-safe scalars/sequences, but coerce
|
|
177
|
+
tuples to lists so json.dumps is happy."""
|
|
178
|
+
if isinstance(obj, dict):
|
|
179
|
+
return {k: _jsonable(v) for k, v in obj.items()}
|
|
180
|
+
if isinstance(obj, (list, tuple)):
|
|
181
|
+
return [_jsonable(v) for v in obj]
|
|
182
|
+
return obj
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Wire up the provider instrumentors.
|
|
2
|
+
|
|
3
|
+
We wrap the maintained openllmetry (Traceloop) instrumentors. Each is optional:
|
|
4
|
+
if the provider SDK (and its instrumentor extra) isn't installed, we skip it
|
|
5
|
+
silently so a customer who only uses Anthropic doesn't need OpenAI installed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("blitz")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def instrument_providers(tracer_provider: TracerProvider) -> list[str]:
|
|
18
|
+
"""Instrument every supported provider that is importable. Returns the
|
|
19
|
+
list of provider names that were successfully instrumented."""
|
|
20
|
+
instrumented: list[str] = []
|
|
21
|
+
|
|
22
|
+
def _try(name: str, importer) -> None:
|
|
23
|
+
try:
|
|
24
|
+
instrumentor = importer()
|
|
25
|
+
instrumentor.instrument(tracer_provider=tracer_provider)
|
|
26
|
+
instrumented.append(name)
|
|
27
|
+
except ImportError as exc:
|
|
28
|
+
logger.debug("blitz: %s instrumentation unavailable (%s)", name, exc)
|
|
29
|
+
except Exception: # noqa: BLE001 - never let instrumentation crash the app
|
|
30
|
+
logger.warning("blitz: failed to instrument %s", name, exc_info=True)
|
|
31
|
+
|
|
32
|
+
def _openai():
|
|
33
|
+
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
|
|
34
|
+
|
|
35
|
+
return OpenAIInstrumentor()
|
|
36
|
+
|
|
37
|
+
def _anthropic():
|
|
38
|
+
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
|
39
|
+
|
|
40
|
+
return AnthropicInstrumentor()
|
|
41
|
+
|
|
42
|
+
def _gemini():
|
|
43
|
+
from opentelemetry.instrumentation.google_generativeai import (
|
|
44
|
+
GoogleGenerativeAiInstrumentor,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return GoogleGenerativeAiInstrumentor()
|
|
48
|
+
|
|
49
|
+
_try("openai", _openai)
|
|
50
|
+
_try("anthropic", _anthropic)
|
|
51
|
+
_try("gemini", _gemini)
|
|
52
|
+
return instrumented
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Minimal blitz example.
|
|
2
|
+
|
|
3
|
+
Install:
|
|
4
|
+
pip install -e '.[anthropic]' # or .[all] for all three providers
|
|
5
|
+
|
|
6
|
+
Run:
|
|
7
|
+
export BLITZ_API_KEY=sk_...
|
|
8
|
+
python examples/quickstart.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import anthropic
|
|
14
|
+
|
|
15
|
+
import blitz
|
|
16
|
+
|
|
17
|
+
blitz.init(
|
|
18
|
+
project_id="proj_demo",
|
|
19
|
+
api_key=os.environ["BLITZ_API_KEY"],
|
|
20
|
+
endpoint=os.environ.get("BLITZ_ENDPOINT", "http://localhost:8000"),
|
|
21
|
+
sample_rate=1.0, # trace everything in the demo
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
|
25
|
+
|
|
26
|
+
resp = client.messages.create(
|
|
27
|
+
model="claude-haiku-4-5-20251001",
|
|
28
|
+
max_tokens=128,
|
|
29
|
+
messages=[{"role": "user", "content": "In one sentence, what is distributed tracing?"}],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
print(resp.content[0].text)
|
|
33
|
+
print("\n--> Open your blitz dashboard; this call is now a trace.")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "blitz-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Drop-in OpenTelemetry tracing for OpenAI, Anthropic, and Gemini LLM calls. Ships full I/O, token usage, model, latency, and cost to your blitz backend."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"opentelemetry-api>=1.27.0",
|
|
9
|
+
"opentelemetry-sdk>=1.27.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Provider instrumentors are optional extras — install only the ones you use.
|
|
13
|
+
# We wrap the maintained openllmetry (Traceloop) instrumentors rather than
|
|
14
|
+
# hand-rolling per-provider patching.
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
openai = ["opentelemetry-instrumentation-openai>=0.33.0"]
|
|
17
|
+
anthropic = ["opentelemetry-instrumentation-anthropic>=0.33.0"]
|
|
18
|
+
gemini = ["opentelemetry-instrumentation-google-generativeai>=0.33.0"]
|
|
19
|
+
all = [
|
|
20
|
+
"opentelemetry-instrumentation-openai>=0.33.0",
|
|
21
|
+
"opentelemetry-instrumentation-anthropic>=0.33.0",
|
|
22
|
+
"opentelemetry-instrumentation-google-generativeai>=0.33.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["blitz"]
|