spanlens 0.1.0__py3-none-any.whl

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.
spanlens/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """Spanlens — agent tracing, LLM usage capture, and cost observability.
2
+
3
+ Quick start::
4
+
5
+ from spanlens import SpanlensClient
6
+
7
+ client = SpanlensClient(api_key="sl_live_...")
8
+
9
+ with client.start_trace("rag_pipeline") as trace:
10
+ with trace.span("retrieval", span_type="retrieval") as span:
11
+ docs = retrieve(query)
12
+ span.end(output=docs)
13
+
14
+ with trace.span("generation", span_type="llm") as span:
15
+ from openai import OpenAI
16
+ response = OpenAI().chat.completions.create(...)
17
+ span.end(
18
+ output=response.choices[0].message.content,
19
+ prompt_tokens=response.usage.prompt_tokens,
20
+ completion_tokens=response.usage.completion_tokens,
21
+ total_tokens=response.usage.total_tokens,
22
+ )
23
+
24
+ For proxy-mode (zero-code) instrumentation, see
25
+ ``spanlens.integrations.openai`` / ``anthropic`` / ``gemini``.
26
+ """
27
+
28
+ from .client import SpanlensClient
29
+ from .observe import observe, observe_anthropic, observe_gemini, observe_openai
30
+ from .parsers import parse_anthropic_usage, parse_gemini_usage, parse_openai_usage
31
+ from .span import SpanHandle
32
+ from .trace import TraceHandle
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ __all__ = [
37
+ "SpanHandle",
38
+ "SpanlensClient",
39
+ "TraceHandle",
40
+ "__version__",
41
+ "observe",
42
+ "observe_anthropic",
43
+ "observe_gemini",
44
+ "observe_openai",
45
+ "parse_anthropic_usage",
46
+ "parse_gemini_usage",
47
+ "parse_openai_usage",
48
+ ]
spanlens/client.py ADDED
@@ -0,0 +1,89 @@
1
+ """Spanlens SDK entry point — wraps the transport and exposes ``start_trace()``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable, Optional
6
+
7
+ from .trace import TraceHandle, create_trace
8
+ from .transport import Transport
9
+
10
+
11
+ class SpanlensClient:
12
+ """Single entry point for the Spanlens SDK.
13
+
14
+ Example:
15
+ >>> from spanlens import SpanlensClient
16
+ >>> client = SpanlensClient(api_key="sl_live_...")
17
+ >>> trace = client.start_trace("chat_session", metadata={"user_id": "u_42"})
18
+ >>> span = trace.span("call_openai", span_type="llm")
19
+ >>> # ... do work ...
20
+ >>> span.end(total_tokens=150, cost_usd=0.0023)
21
+ >>> trace.end(status="completed")
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ api_key: str,
27
+ *,
28
+ base_url: Optional[str] = None,
29
+ timeout_ms: int = 3000,
30
+ silent: bool = True,
31
+ on_error: Optional[Callable[[BaseException, str], None]] = None,
32
+ ) -> None:
33
+ if not api_key or not api_key.strip():
34
+ raise ValueError("[spanlens] api_key is required")
35
+
36
+ config: dict[str, Any] = {
37
+ "api_key": api_key,
38
+ "timeout_ms": timeout_ms,
39
+ "silent": silent,
40
+ }
41
+ if base_url is not None:
42
+ config["base_url"] = base_url
43
+ if on_error is not None:
44
+ config["on_error"] = on_error
45
+
46
+ self._transport = Transport(config)
47
+
48
+ def start_trace(
49
+ self,
50
+ name: str,
51
+ *,
52
+ metadata: Optional[dict[str, Any]] = None,
53
+ ) -> TraceHandle:
54
+ """Start a new trace.
55
+
56
+ Returns immediately — ingest runs in the background. Use the returned
57
+ handle as a context manager to auto-end on scope exit::
58
+
59
+ with client.start_trace("rag_pipeline") as trace:
60
+ with trace.span("retrieval", span_type="retrieval") as span:
61
+ ...
62
+ """
63
+ return create_trace(self._transport, name, metadata)
64
+
65
+ def close(self) -> None:
66
+ """Drain in-flight ingest calls and release the connection pool.
67
+
68
+ Optional — also runs automatically at interpreter shutdown via
69
+ ``atexit``. Call explicitly when you need to guarantee delivery (e.g.
70
+ in short-lived scripts that exit before the background pool flushes).
71
+ """
72
+ self._transport.close()
73
+
74
+ # Allow `with SpanlensClient(...) as c:` for tidy script lifetimes.
75
+ def __enter__(self) -> SpanlensClient:
76
+ return self
77
+
78
+ def __exit__(self, *_exc: object) -> None:
79
+ self.close()
80
+
81
+ # ── Internal escape hatch for the integrations module ───────
82
+
83
+ @property
84
+ def _transport_internal(self) -> Transport:
85
+ """Exposed for wrappers (openai/anthropic auto-instrumentation)."""
86
+ return self._transport
87
+
88
+
89
+ __all__ = ["SpanlensClient"]
@@ -0,0 +1,40 @@
1
+ """Provider integrations — pre-configured clients pointed at the Spanlens proxy.
2
+
3
+ Each helper requires its provider SDK as an *optional* dependency. Install
4
+ with the matching extra::
5
+
6
+ pip install "spanlens[openai]"
7
+ pip install "spanlens[anthropic]"
8
+ pip install "spanlens[gemini]"
9
+ pip install "spanlens[all]"
10
+
11
+ If the provider SDK is missing, the helper raises ``ImportError`` with the
12
+ exact pip command needed.
13
+ """
14
+
15
+ from .anthropic import (
16
+ DEFAULT_SPANLENS_ANTHROPIC_PROXY,
17
+ create_anthropic,
18
+ )
19
+ from .anthropic import (
20
+ with_prompt_version as with_anthropic_prompt_version,
21
+ )
22
+ from .gemini import DEFAULT_SPANLENS_GEMINI_PROXY, create_gemini
23
+ from .openai import (
24
+ DEFAULT_SPANLENS_OPENAI_PROXY,
25
+ create_openai,
26
+ )
27
+ from .openai import (
28
+ with_prompt_version as with_openai_prompt_version,
29
+ )
30
+
31
+ __all__ = [
32
+ "DEFAULT_SPANLENS_ANTHROPIC_PROXY",
33
+ "DEFAULT_SPANLENS_GEMINI_PROXY",
34
+ "DEFAULT_SPANLENS_OPENAI_PROXY",
35
+ "create_anthropic",
36
+ "create_gemini",
37
+ "create_openai",
38
+ "with_anthropic_prompt_version",
39
+ "with_openai_prompt_version",
40
+ ]
@@ -0,0 +1,99 @@
1
+ """Anthropic client helper — pre-configured for the Spanlens proxy.
2
+
3
+ Replaces::
4
+
5
+ from anthropic import Anthropic
6
+ client = Anthropic(
7
+ api_key=os.environ["SPANLENS_API_KEY"],
8
+ base_url="https://spanlens-server.vercel.app/proxy/anthropic",
9
+ )
10
+
11
+ With::
12
+
13
+ from spanlens.integrations.anthropic import create_anthropic
14
+ client = create_anthropic()
15
+
16
+ ``anthropic`` is an *optional* dependency — install with
17
+ ``pip install "spanlens[anthropic]"``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ from typing import Any, Optional
24
+
25
+ DEFAULT_SPANLENS_ANTHROPIC_PROXY = "https://spanlens-server.vercel.app/proxy/anthropic"
26
+ PROMPT_VERSION_HEADER = "x-spanlens-prompt-version"
27
+
28
+
29
+ def create_anthropic(
30
+ *,
31
+ api_key: Optional[str] = None,
32
+ base_url: Optional[str] = None,
33
+ **kwargs: Any,
34
+ ) -> Any:
35
+ """Build an ``anthropic.Anthropic`` client whose requests flow through
36
+ the Spanlens proxy.
37
+
38
+ Args:
39
+ api_key: Spanlens API key. Defaults to ``SPANLENS_API_KEY`` env var.
40
+ base_url: Override the proxy URL — useful for self-hosted Spanlens.
41
+ **kwargs: Forwarded to ``anthropic.Anthropic(...)`` unchanged.
42
+
43
+ Raises:
44
+ ImportError: ``anthropic`` package is not installed.
45
+ ValueError: ``api_key`` not provided and ``SPANLENS_API_KEY`` unset.
46
+ """
47
+ try:
48
+ from anthropic import Anthropic
49
+ except ImportError as exc: # pragma: no cover - import-time
50
+ raise ImportError(
51
+ "[spanlens] The `anthropic` package is required for create_anthropic(). "
52
+ 'Install with: pip install "spanlens[anthropic]"'
53
+ ) from exc
54
+
55
+ resolved_key = api_key or os.environ.get("SPANLENS_API_KEY")
56
+ if not resolved_key:
57
+ raise ValueError(
58
+ "[spanlens] SPANLENS_API_KEY is not set. Pass api_key=... to "
59
+ "create_anthropic() or set the SPANLENS_API_KEY environment variable."
60
+ )
61
+
62
+ return Anthropic(
63
+ api_key=resolved_key,
64
+ base_url=base_url or DEFAULT_SPANLENS_ANTHROPIC_PROXY,
65
+ **kwargs,
66
+ )
67
+
68
+
69
+ def with_prompt_version(prompt_version: str) -> dict[str, dict[str, str]]:
70
+ """Tag a single Anthropic request with a Spanlens prompt version.
71
+
72
+ Example::
73
+
74
+ from spanlens.integrations.anthropic import (
75
+ create_anthropic,
76
+ with_prompt_version,
77
+ )
78
+
79
+ client = create_anthropic()
80
+ msg = client.messages.create(
81
+ model="claude-3-5-sonnet-20241022",
82
+ max_tokens=1024,
83
+ messages=[...],
84
+ **with_prompt_version("greeter@latest"),
85
+ )
86
+
87
+ Args:
88
+ prompt_version: Raw UUID, ``"<name>@<version>"`` or
89
+ ``"<name>@latest"``.
90
+ """
91
+ return {"extra_headers": {PROMPT_VERSION_HEADER: prompt_version}}
92
+
93
+
94
+ __all__ = [
95
+ "DEFAULT_SPANLENS_ANTHROPIC_PROXY",
96
+ "PROMPT_VERSION_HEADER",
97
+ "create_anthropic",
98
+ "with_prompt_version",
99
+ ]
@@ -0,0 +1,119 @@
1
+ """Google Gemini client helper — pre-configured for the Spanlens proxy.
2
+
3
+ The Google ``google-generativeai`` SDK uses a *module-level* configuration
4
+ pattern (``genai.configure(api_key=..., transport=...)``) and routes through
5
+ its own internal HTTP client; it does **not** support a per-instance
6
+ ``base_url`` the way OpenAI/Anthropic do.
7
+
8
+ To make the proxy approach work, we expose ``create_gemini()`` which returns
9
+ an ``httpx.Client`` pre-configured with the Spanlens proxy as ``base_url``
10
+ plus an ``Authorization`` header carrying the Spanlens API key. The proxy
11
+ in turn forwards to ``generativelanguage.googleapis.com`` and decodes the
12
+ provider key on the server.
13
+
14
+ Use the returned client to issue raw HTTP calls, OR use the proxy URL +
15
+ your own ``google.generativeai`` ``client_options.api_endpoint`` override.
16
+
17
+ ``google-generativeai`` is an *optional* dependency for the
18
+ ``configure_gemini()`` variant — install with
19
+ ``pip install "spanlens[gemini]"``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ from typing import Any, Optional
26
+
27
+ import httpx
28
+
29
+ DEFAULT_SPANLENS_GEMINI_PROXY = "https://spanlens-server.vercel.app/proxy/gemini"
30
+
31
+
32
+ def create_gemini(
33
+ *,
34
+ api_key: Optional[str] = None,
35
+ base_url: Optional[str] = None,
36
+ timeout: float = 60.0,
37
+ ) -> httpx.Client:
38
+ """Return an ``httpx.Client`` pre-configured for the Spanlens Gemini proxy.
39
+
40
+ Use it to issue REST calls to Gemini through Spanlens::
41
+
42
+ client = create_gemini()
43
+ res = client.post(
44
+ "/v1beta/models/gemini-1.5-flash:generateContent",
45
+ json={"contents": [{"parts": [{"text": "Hello"}]}]},
46
+ )
47
+ print(res.json())
48
+
49
+ Args:
50
+ api_key: Spanlens API key. Defaults to ``SPANLENS_API_KEY`` env var.
51
+ base_url: Override the proxy URL — useful for self-hosted Spanlens.
52
+ timeout: Request timeout in seconds (LLM calls can be slow — default
53
+ 60s).
54
+
55
+ Raises:
56
+ ValueError: ``api_key`` not provided and ``SPANLENS_API_KEY`` unset.
57
+ """
58
+ resolved_key = api_key or os.environ.get("SPANLENS_API_KEY")
59
+ if not resolved_key:
60
+ raise ValueError(
61
+ "[spanlens] SPANLENS_API_KEY is not set. Pass api_key=... to "
62
+ "create_gemini() or set the SPANLENS_API_KEY environment variable."
63
+ )
64
+
65
+ return httpx.Client(
66
+ base_url=base_url or DEFAULT_SPANLENS_GEMINI_PROXY,
67
+ headers={"Authorization": f"Bearer {resolved_key}"},
68
+ timeout=timeout,
69
+ )
70
+
71
+
72
+ def configure_gemini(
73
+ *,
74
+ api_key: Optional[str] = None,
75
+ base_url: Optional[str] = None,
76
+ ) -> None:
77
+ """Point ``google.generativeai`` at the Spanlens proxy globally.
78
+
79
+ This calls ``genai.configure()`` with the Spanlens proxy as the API
80
+ endpoint. After calling, all ``genai.GenerativeModel(...).generate_content(...)``
81
+ calls flow through Spanlens.
82
+
83
+ Note:
84
+ ``google.generativeai`` configures the endpoint *globally* per
85
+ process — use this in single-tenant scripts. For multi-tenant
86
+ servers, prefer the ``create_gemini()`` raw-HTTP client.
87
+
88
+ Raises:
89
+ ImportError: ``google-generativeai`` package is not installed.
90
+ ValueError: ``api_key`` not provided and ``SPANLENS_API_KEY`` unset.
91
+ """
92
+ try:
93
+ import google.generativeai as genai # type: ignore[import-not-found]
94
+ except ImportError as exc: # pragma: no cover - import-time
95
+ raise ImportError(
96
+ "[spanlens] The `google-generativeai` package is required for "
97
+ 'configure_gemini(). Install with: pip install "spanlens[gemini]"'
98
+ ) from exc
99
+
100
+ resolved_key = api_key or os.environ.get("SPANLENS_API_KEY")
101
+ if not resolved_key:
102
+ raise ValueError(
103
+ "[spanlens] SPANLENS_API_KEY is not set. Pass api_key=... to "
104
+ "configure_gemini() or set the SPANLENS_API_KEY environment variable."
105
+ )
106
+
107
+ proxy = base_url or DEFAULT_SPANLENS_GEMINI_PROXY
108
+
109
+ # google.generativeai accepts api_endpoint via client_options. The path
110
+ # prefix (e.g. /v1beta) is added by the SDK; we only configure the host.
111
+ client_options: Any = {"api_endpoint": proxy}
112
+ genai.configure(api_key=resolved_key, client_options=client_options)
113
+
114
+
115
+ __all__ = [
116
+ "DEFAULT_SPANLENS_GEMINI_PROXY",
117
+ "configure_gemini",
118
+ "create_gemini",
119
+ ]
@@ -0,0 +1,106 @@
1
+ """OpenAI client helper — pre-configured for the Spanlens proxy.
2
+
3
+ Replaces::
4
+
5
+ from openai import OpenAI
6
+ client = OpenAI(
7
+ api_key=os.environ["SPANLENS_API_KEY"],
8
+ base_url="https://spanlens-server.vercel.app/proxy/openai/v1",
9
+ )
10
+
11
+ With::
12
+
13
+ from spanlens.integrations.openai import create_openai
14
+ client = create_openai()
15
+
16
+ The returned client behaves identically to ``OpenAI(...)`` — only the
17
+ ``base_url`` is redirected to the Spanlens proxy (which records the call,
18
+ enforces quota, computes cost) and ``api_key`` defaults to
19
+ ``SPANLENS_API_KEY`` from the environment.
20
+
21
+ ``openai`` is an *optional* dependency — install with
22
+ ``pip install "spanlens[openai]"``.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ from typing import Any, Optional
29
+
30
+ DEFAULT_SPANLENS_OPENAI_PROXY = "https://spanlens-server.vercel.app/proxy/openai/v1"
31
+ PROMPT_VERSION_HEADER = "x-spanlens-prompt-version"
32
+
33
+
34
+ def create_openai(
35
+ *,
36
+ api_key: Optional[str] = None,
37
+ base_url: Optional[str] = None,
38
+ **kwargs: Any,
39
+ ) -> Any:
40
+ """Build an ``openai.OpenAI`` client whose requests flow through the
41
+ Spanlens proxy.
42
+
43
+ Args:
44
+ api_key: Spanlens API key. Defaults to ``SPANLENS_API_KEY`` env var.
45
+ base_url: Override the proxy URL — useful for self-hosted Spanlens.
46
+ **kwargs: Forwarded to ``openai.OpenAI(...)`` unchanged.
47
+
48
+ Raises:
49
+ ImportError: ``openai`` package is not installed.
50
+ ValueError: ``api_key`` not provided and ``SPANLENS_API_KEY`` unset.
51
+ """
52
+ try:
53
+ from openai import OpenAI
54
+ except ImportError as exc: # pragma: no cover - import-time
55
+ raise ImportError(
56
+ "[spanlens] The `openai` package is required for create_openai(). "
57
+ 'Install with: pip install "spanlens[openai]"'
58
+ ) from exc
59
+
60
+ resolved_key = api_key or os.environ.get("SPANLENS_API_KEY")
61
+ if not resolved_key:
62
+ raise ValueError(
63
+ "[spanlens] SPANLENS_API_KEY is not set. Pass api_key=... to "
64
+ "create_openai() or set the SPANLENS_API_KEY environment variable."
65
+ )
66
+
67
+ return OpenAI(
68
+ api_key=resolved_key,
69
+ base_url=base_url or DEFAULT_SPANLENS_OPENAI_PROXY,
70
+ **kwargs,
71
+ )
72
+
73
+
74
+ def with_prompt_version(prompt_version: str) -> dict[str, dict[str, str]]:
75
+ """Tag a single OpenAI request with a Spanlens prompt version.
76
+
77
+ Spread the result into the per-request ``extra_headers``::
78
+
79
+ from openai import OpenAI
80
+ from spanlens.integrations.openai import (
81
+ create_openai,
82
+ with_prompt_version,
83
+ )
84
+
85
+ client = create_openai()
86
+ res = client.chat.completions.create(
87
+ model="gpt-4o-mini",
88
+ messages=[...],
89
+ **with_prompt_version("chatbot-system@3"),
90
+ )
91
+
92
+ Args:
93
+ prompt_version: Either a raw ``prompt_versions.id`` UUID,
94
+ ``"<name>@<version>"`` (e.g. ``"chatbot-system@3"``), or
95
+ ``"<name>@latest"`` to always resolve to the latest version
96
+ server-side.
97
+ """
98
+ return {"extra_headers": {PROMPT_VERSION_HEADER: prompt_version}}
99
+
100
+
101
+ __all__ = [
102
+ "DEFAULT_SPANLENS_OPENAI_PROXY",
103
+ "PROMPT_VERSION_HEADER",
104
+ "create_openai",
105
+ "with_prompt_version",
106
+ ]