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 +48 -0
- spanlens/client.py +89 -0
- spanlens/integrations/__init__.py +40 -0
- spanlens/integrations/anthropic.py +99 -0
- spanlens/integrations/gemini.py +119 -0
- spanlens/integrations/openai.py +106 -0
- spanlens/observe.py +243 -0
- spanlens/parsers.py +141 -0
- spanlens/span.py +231 -0
- spanlens/trace.py +156 -0
- spanlens/transport.py +207 -0
- spanlens/types.py +98 -0
- spanlens-0.1.0.dist-info/METADATA +261 -0
- spanlens-0.1.0.dist-info/RECORD +16 -0
- spanlens-0.1.0.dist-info/WHEEL +4 -0
- spanlens-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
]
|