logspine 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.
@@ -0,0 +1,52 @@
1
+ # dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # build outputs
6
+ dist/
7
+ .next/
8
+ .turbo/
9
+ out/
10
+ build/
11
+
12
+ # env
13
+ .env
14
+ .env.local
15
+ .env.*.local
16
+
17
+ # logs
18
+ npm-debug.log*
19
+ yarn-debug.log*
20
+ yarn-error.log*
21
+ pnpm-debug.log*
22
+
23
+ # OS
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # editor
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+
32
+ # wrangler / CF
33
+ .wrangler/
34
+ .dev.vars
35
+
36
+ # tsbuildinfo
37
+ *.tsbuildinfo
38
+
39
+ # coverage
40
+ coverage/
41
+
42
+ # Tinybird CLI local state (contains admin token!)
43
+ .tinyb
44
+ .tinyenv
45
+
46
+ # pnpm temp files from interrupted installs
47
+ _tmp_*
48
+
49
+ # outreach tool outputs (may contain PII, large files)
50
+ tools/helicone-stargazers-raw.json
51
+ tools/helicone-stargazers-filtered.json
52
+ tools/helicone-stargazers-filtered.csv
logspine-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Kallen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include logspine *.py
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: logspine
3
+ Version: 0.1.0
4
+ Summary: Drop-in observability for AI agents — Python SDK
5
+ Project-URL: Homepage, https://www.logspine.dev
6
+ Project-URL: Documentation, https://www.logspine.dev/docs
7
+ Project-URL: Repository, https://github.com/logspine-dev/logspine
8
+ Author-email: Adam Kallen <hello@logspine.dev>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.9
12
+ Provides-Extra: anthropic
13
+ Requires-Dist: anthropic>=0.25.0; extra == 'anthropic'
14
+ Provides-Extra: openai
15
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # logspine · Python SDK
19
+
20
+ Drop-in observability for AI agents. Track every OpenAI and Anthropic call
21
+ with cost, tokens, and latency — no code changes to your prompts.
22
+
23
+ Full docs: [logspine.dev/docs](https://www.logspine.dev/docs)
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install logspine
31
+
32
+ # with OpenAI support
33
+ pip install "logspine[openai]"
34
+
35
+ # with Anthropic support
36
+ pip install "logspine[anthropic]"
37
+
38
+ # both
39
+ pip install "logspine[openai,anthropic]"
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Quick start — OpenAI
45
+
46
+ ```python
47
+ import openai
48
+ from logspine import LogspineClient
49
+
50
+ logspine = LogspineClient(api_key="lsk_live_...")
51
+ client = logspine.instrument_openai(openai.OpenAI())
52
+
53
+ # Every call is now tracked automatically
54
+ response = client.chat.completions.create(
55
+ model="gpt-4o-mini",
56
+ messages=[{"role": "user", "content": "Say hello in five words."}],
57
+ )
58
+ print(response.choices[0].message.content)
59
+
60
+ logspine.flush() # call before process exit
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quick start — Anthropic
66
+
67
+ ```python
68
+ import anthropic
69
+ from logspine import LogspineClient
70
+
71
+ logspine = LogspineClient(api_key="lsk_live_...")
72
+ client = logspine.instrument_anthropic(anthropic.Anthropic())
73
+
74
+ response = client.messages.create(
75
+ model="claude-sonnet-4-5",
76
+ max_tokens=256,
77
+ messages=[{"role": "user", "content": "Say hello in five words."}],
78
+ )
79
+ print(response.content[0].text)
80
+
81
+ logspine.flush()
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Optional span metadata
87
+
88
+ Pass a `logspine` dict as an extra kwarg to tag individual calls:
89
+
90
+ ```python
91
+ client.chat.completions.create(
92
+ model="gpt-4o",
93
+ messages=[...],
94
+ logspine={
95
+ "trace_id": "my-session-abc123",
96
+ "name": "summarise_document",
97
+ "user_id": "usr_42",
98
+ },
99
+ )
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Links
105
+
106
+ - [Docs](https://www.logspine.dev/docs)
107
+ - [Dashboard](https://www.logspine.dev/dashboard)
108
+ - [Pricing](https://www.logspine.dev/pricing)
109
+ - [GitHub](https://github.com/logspine-dev/logspine)
@@ -0,0 +1,92 @@
1
+ # logspine · Python SDK
2
+
3
+ Drop-in observability for AI agents. Track every OpenAI and Anthropic call
4
+ with cost, tokens, and latency — no code changes to your prompts.
5
+
6
+ Full docs: [logspine.dev/docs](https://www.logspine.dev/docs)
7
+
8
+ ---
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install logspine
14
+
15
+ # with OpenAI support
16
+ pip install "logspine[openai]"
17
+
18
+ # with Anthropic support
19
+ pip install "logspine[anthropic]"
20
+
21
+ # both
22
+ pip install "logspine[openai,anthropic]"
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Quick start — OpenAI
28
+
29
+ ```python
30
+ import openai
31
+ from logspine import LogspineClient
32
+
33
+ logspine = LogspineClient(api_key="lsk_live_...")
34
+ client = logspine.instrument_openai(openai.OpenAI())
35
+
36
+ # Every call is now tracked automatically
37
+ response = client.chat.completions.create(
38
+ model="gpt-4o-mini",
39
+ messages=[{"role": "user", "content": "Say hello in five words."}],
40
+ )
41
+ print(response.choices[0].message.content)
42
+
43
+ logspine.flush() # call before process exit
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Quick start — Anthropic
49
+
50
+ ```python
51
+ import anthropic
52
+ from logspine import LogspineClient
53
+
54
+ logspine = LogspineClient(api_key="lsk_live_...")
55
+ client = logspine.instrument_anthropic(anthropic.Anthropic())
56
+
57
+ response = client.messages.create(
58
+ model="claude-sonnet-4-5",
59
+ max_tokens=256,
60
+ messages=[{"role": "user", "content": "Say hello in five words."}],
61
+ )
62
+ print(response.content[0].text)
63
+
64
+ logspine.flush()
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Optional span metadata
70
+
71
+ Pass a `logspine` dict as an extra kwarg to tag individual calls:
72
+
73
+ ```python
74
+ client.chat.completions.create(
75
+ model="gpt-4o",
76
+ messages=[...],
77
+ logspine={
78
+ "trace_id": "my-session-abc123",
79
+ "name": "summarise_document",
80
+ "user_id": "usr_42",
81
+ },
82
+ )
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Links
88
+
89
+ - [Docs](https://www.logspine.dev/docs)
90
+ - [Dashboard](https://www.logspine.dev/dashboard)
91
+ - [Pricing](https://www.logspine.dev/pricing)
92
+ - [GitHub](https://github.com/logspine-dev/logspine)
@@ -0,0 +1,4 @@
1
+ from .client import LogspineClient
2
+ from ._version import __version__
3
+
4
+ __all__ = ["LogspineClient", "__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,117 @@
1
+ """
2
+ Logspine Python SDK — AI agent observability.
3
+ https://www.logspine.dev
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ import json
10
+ import threading
11
+ from typing import Any, Optional
12
+ from ._version import __version__
13
+
14
+ DEFAULT_ENDPOINT = "https://ingest.logspine.dev"
15
+ FLUSH_INTERVAL = 2.0 # seconds
16
+ MAX_BATCH_SIZE = 100
17
+
18
+
19
+ class LogspineClient:
20
+ """
21
+ Drop-in observability client for AI agents.
22
+
23
+ Usage::
24
+
25
+ from logspine import LogspineClient
26
+ import openai
27
+
28
+ logspine = LogspineClient(api_key="lsk_live_...")
29
+ client = logspine.instrument_openai(openai.OpenAI())
30
+
31
+ # Every call is now tracked
32
+ response = client.chat.completions.create(...)
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ api_key: str,
38
+ endpoint: str = DEFAULT_ENDPOINT,
39
+ flush_interval: float = FLUSH_INTERVAL,
40
+ ) -> None:
41
+ if not api_key:
42
+ raise ValueError("api_key is required")
43
+ self._api_key = api_key
44
+ self._endpoint = endpoint.rstrip("/")
45
+ self._queue: list[dict[str, Any]] = []
46
+ self._lock = threading.Lock()
47
+ self._flush_interval = flush_interval
48
+ self._timer: Optional[threading.Timer] = None
49
+ self._schedule_flush()
50
+
51
+ # ── Instrumentation ──────────────────────────────────────────────
52
+
53
+ def instrument_openai(self, client: Any) -> Any:
54
+ """Wrap an OpenAI client to track every call automatically."""
55
+ from .instrumentation.openai import instrument
56
+ return instrument(client, self)
57
+
58
+ def instrument_anthropic(self, client: Any) -> Any:
59
+ """Wrap an Anthropic client to track every call automatically."""
60
+ from .instrumentation.anthropic import instrument
61
+ return instrument(client, self)
62
+
63
+ # ── Span tracking ────────────────────────────────────────────────
64
+
65
+ def _record_span(self, span: dict[str, Any]) -> None:
66
+ with self._lock:
67
+ self._queue.append(span)
68
+ if len(self._queue) >= MAX_BATCH_SIZE:
69
+ self._flush_locked()
70
+
71
+ def flush(self) -> None:
72
+ """Send all buffered spans immediately. Call before process exit."""
73
+ with self._lock:
74
+ self._flush_locked()
75
+
76
+ def _flush_locked(self) -> None:
77
+ if not self._queue:
78
+ return
79
+ batch = self._queue[:]
80
+ self._queue.clear()
81
+ # Fire and forget in a thread to not block the caller
82
+ threading.Thread(target=self._send, args=(batch,), daemon=True).start()
83
+
84
+ def _schedule_flush(self) -> None:
85
+ self._timer = threading.Timer(self._flush_interval, self._auto_flush)
86
+ self._timer.daemon = True
87
+ self._timer.start()
88
+
89
+ def _auto_flush(self) -> None:
90
+ self.flush()
91
+ self._schedule_flush()
92
+
93
+ def _send(self, spans: list[dict[str, Any]]) -> None:
94
+ import urllib.request
95
+ import urllib.error
96
+
97
+ payload = json.dumps({"spans": spans}).encode()
98
+ req = urllib.request.Request(
99
+ f"{self._endpoint}/v1/ingest",
100
+ data=payload,
101
+ headers={
102
+ "Content-Type": "application/json",
103
+ "x-logspine-key": self._api_key,
104
+ "x-logspine-sdk": f"python/{__version__}",
105
+ },
106
+ method="POST",
107
+ )
108
+ try:
109
+ with urllib.request.urlopen(req, timeout=5):
110
+ pass
111
+ except Exception:
112
+ pass # Never raise — observability must never break the app
113
+
114
+ def __del__(self) -> None:
115
+ if self._timer:
116
+ self._timer.cancel()
117
+ self.flush()
File without changes
@@ -0,0 +1,109 @@
1
+ """Anthropic instrumentation for Logspine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import json
7
+ from typing import Any, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from ..client import LogspineClient
11
+
12
+ # Cost per 1M tokens in USD (as of mid-2026)
13
+ PRICING: dict[str, dict[str, float]] = {
14
+ "claude-opus-4": {"input": 15.00, "output": 75.00},
15
+ "claude-sonnet-4": {"input": 3.00, "output": 15.00},
16
+ "claude-3-5-sonnet": {"input": 3.00, "output": 15.00},
17
+ "claude-3-5-haiku": {"input": 0.80, "output": 4.00},
18
+ "claude-3-haiku": {"input": 0.25, "output": 1.25},
19
+ }
20
+
21
+
22
+ def _cost(model: str, input_tokens: int, output_tokens: int) -> float:
23
+ key = next((k for k in PRICING if model.startswith(k)), None)
24
+ if not key:
25
+ return 0.0
26
+ p = PRICING[key]
27
+ return (input_tokens * p["input"] + output_tokens * p["output"]) / 1_000_000
28
+
29
+
30
+ def instrument(anthropic_client: Any, logspine: "LogspineClient") -> Any:
31
+ """Wrap an anthropic.Anthropic() instance to track every LLM call."""
32
+ original_create = anthropic_client.messages.create
33
+
34
+ def tracked_create(*args: Any, **kwargs: Any) -> Any:
35
+ meta: dict[str, Any] = kwargs.pop("logspine", {}) or {}
36
+ start = time.time()
37
+ error_msg = ""
38
+ response = None
39
+
40
+ try:
41
+ response = original_create(*args, **kwargs)
42
+ except Exception as exc:
43
+ error_msg = str(exc)
44
+ raise
45
+ finally:
46
+ end = time.time()
47
+
48
+ model = kwargs.get("model", "")
49
+ messages = kwargs.get("messages", [])
50
+ system = kwargs.get("system", "")
51
+
52
+ input_tokens = 0
53
+ output_tokens = 0
54
+ output_text = ""
55
+
56
+ if response is not None:
57
+ usage = getattr(response, "usage", None)
58
+ if usage:
59
+ input_tokens = getattr(usage, "input_tokens", 0) or 0
60
+ output_tokens = getattr(usage, "output_tokens", 0) or 0
61
+ content = getattr(response, "content", [])
62
+ if content:
63
+ first = content[0]
64
+ output_text = getattr(first, "text", "") or ""
65
+
66
+ cost = _cost(model, input_tokens, output_tokens)
67
+
68
+ # Build input representation — include system prompt if present
69
+ input_payload: dict[str, Any] = {"messages": messages}
70
+ if system:
71
+ input_payload["system"] = system
72
+
73
+ span: dict[str, Any] = {
74
+ "trace_id": meta.get("trace_id", _new_id()),
75
+ "span_id": _new_id(),
76
+ "parent_id": meta.get("parent_id", ""),
77
+ "name": meta.get("name", f"messages/{model}"),
78
+ "start_ts": _iso(start),
79
+ "end_ts": _iso(end),
80
+ "provider": "anthropic",
81
+ "model": model,
82
+ "prompt_tokens": input_tokens,
83
+ "completion_tokens": output_tokens,
84
+ "cost_usd": cost,
85
+ "status": "error" if error_msg else "ok",
86
+ "error": error_msg,
87
+ "input": json.dumps(input_payload)[:65536],
88
+ "output": output_text[:65536],
89
+ "metadata": json.dumps({
90
+ k: v for k, v in meta.items()
91
+ if k not in ("trace_id", "span_id", "parent_id", "name")
92
+ }),
93
+ }
94
+ logspine._record_span(span)
95
+
96
+ return response
97
+
98
+ anthropic_client.messages.create = tracked_create
99
+ return anthropic_client
100
+
101
+
102
+ def _new_id() -> str:
103
+ import uuid
104
+ return str(uuid.uuid4())
105
+
106
+
107
+ def _iso(ts: float) -> str:
108
+ from datetime import datetime, timezone
109
+ return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
@@ -0,0 +1,106 @@
1
+ """OpenAI instrumentation for Logspine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import json
7
+ from typing import Any, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from ..client import LogspineClient
11
+
12
+ # Cost per 1M tokens in USD (as of mid-2026)
13
+ PRICING: dict[str, dict[str, float]] = {
14
+ "gpt-4o": {"input": 2.50, "output": 10.00},
15
+ "gpt-4o-mini": {"input": 0.15, "output": 0.60},
16
+ "gpt-4-turbo": {"input": 10.00, "output": 30.00},
17
+ "gpt-3.5-turbo": {"input": 0.50, "output": 1.50},
18
+ "o1": {"input": 15.00, "output": 60.00},
19
+ "o1-mini": {"input": 3.00, "output": 12.00},
20
+ }
21
+
22
+
23
+ def _cost(model: str, prompt: int, completion: int) -> float:
24
+ key = next((k for k in PRICING if model.startswith(k)), None)
25
+ if not key:
26
+ return 0.0
27
+ p = PRICING[key]
28
+ return (prompt * p["input"] + completion * p["output"]) / 1_000_000
29
+
30
+
31
+ def instrument(openai_client: Any, logspine: "LogspineClient") -> Any:
32
+ """Wrap an openai.OpenAI() instance to track every LLM call."""
33
+ original_create = openai_client.chat.completions.create
34
+
35
+ def tracked_create(*args: Any, **kwargs: Any) -> Any:
36
+ meta: dict[str, Any] = kwargs.pop("logspine", {}) or {}
37
+ start = time.time()
38
+ error_msg = ""
39
+ response = None
40
+
41
+ try:
42
+ response = original_create(*args, **kwargs)
43
+ except Exception as exc:
44
+ error_msg = str(exc)
45
+ raise
46
+ finally:
47
+ end = time.time()
48
+ duration_ms = int((end - start) * 1000)
49
+
50
+ model = kwargs.get("model", "")
51
+ messages = kwargs.get("messages", [])
52
+
53
+ prompt_tokens = 0
54
+ completion_tokens = 0
55
+ output_text = ""
56
+
57
+ if response is not None:
58
+ usage = getattr(response, "usage", None)
59
+ if usage:
60
+ prompt_tokens = getattr(usage, "prompt_tokens", 0) or 0
61
+ completion_tokens = getattr(usage, "completion_tokens", 0) or 0
62
+ choices = getattr(response, "choices", [])
63
+ if choices:
64
+ msg = getattr(choices[0], "message", None)
65
+ if msg:
66
+ output_text = getattr(msg, "content", "") or ""
67
+
68
+ cost = _cost(model, prompt_tokens, completion_tokens)
69
+
70
+ span: dict[str, Any] = {
71
+ "trace_id": meta.get("trace_id", _new_id()),
72
+ "span_id": _new_id(),
73
+ "parent_id": meta.get("parent_id", ""),
74
+ "name": meta.get("name", f"chat/{model}"),
75
+ "start_ts": _iso(start),
76
+ "end_ts": _iso(end),
77
+ "provider": "openai",
78
+ "model": model,
79
+ "prompt_tokens": prompt_tokens,
80
+ "completion_tokens": completion_tokens,
81
+ "cost_usd": cost,
82
+ "status": "error" if error_msg else "ok",
83
+ "error": error_msg,
84
+ "input": json.dumps(messages)[:65536],
85
+ "output": output_text[:65536],
86
+ "metadata": json.dumps({
87
+ k: v for k, v in meta.items()
88
+ if k not in ("trace_id", "span_id", "parent_id", "name")
89
+ }),
90
+ }
91
+ logspine._record_span(span)
92
+
93
+ return response
94
+
95
+ openai_client.chat.completions.create = tracked_create
96
+ return openai_client
97
+
98
+
99
+ def _new_id() -> str:
100
+ import uuid
101
+ return str(uuid.uuid4())
102
+
103
+
104
+ def _iso(ts: float) -> str:
105
+ from datetime import datetime, timezone
106
+ return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "logspine"
7
+ version = "0.1.0"
8
+ description = "Drop-in observability for AI agents — Python SDK"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Adam Kallen", email = "hello@logspine.dev" }]
12
+ requires-python = ">=3.9"
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ openai = ["openai>=1.0.0"]
17
+ anthropic = ["anthropic>=0.25.0"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://www.logspine.dev"
21
+ Documentation = "https://www.logspine.dev/docs"
22
+ Repository = "https://github.com/logspine-dev/logspine"