modelstat-sdk 0.0.1__tar.gz → 0.0.2__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.
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/PKG-INFO +11 -2
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/README.md +10 -1
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/__init__.py +2 -2
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/_version.py +1 -1
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/capture.py +6 -3
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/config.py +6 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/wire.py +14 -5
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/tests/test_capture.py +13 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/tests/test_wire.py +3 -3
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/.gitignore +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/pyproject.toml +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/client.py +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/py.typed +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/redact.py +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/transport.py +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/src/modelstat/worker.py +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/tests/test_client.py +0 -0
- {modelstat_sdk-0.0.1 → modelstat_sdk-0.0.2}/tests/test_redact.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modelstat-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Privacy-first SDK for modelstat — wrap your backend LLM calls and ship redacted usage to a local daemon or the modelstat server, without touching live-request latency.
|
|
5
5
|
Project-URL: Homepage, https://modelstat.ai
|
|
6
6
|
Project-URL: Repository, https://github.com/modelstat/modelstat
|
|
@@ -137,13 +137,22 @@ cfg = Config("msk_live_…", "raw_sdk_openai").with_remote(
|
|
|
137
137
|
)
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
## Taxonomy auto-detection (off by default)
|
|
141
|
+
|
|
142
|
+
modelstat can auto-detect a work-type *taxonomy* over your sessions, but that's tuned for interactive coding sessions — backend LLM usage usually isn't. So for the SDK taxonomy is **off by default**: every batch ships an explicit `auto_taxonomy: false`. Opt in with the config flag:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
cfg = Config("msk_live_…", "raw_sdk_openai")
|
|
146
|
+
cfg.auto_taxonomy = True # force server-side taxonomy auto-detection on
|
|
147
|
+
```
|
|
148
|
+
|
|
140
149
|
## Privacy floor (always on)
|
|
141
150
|
|
|
142
151
|
Before any bytes leave the SDK process — in **every** mode — an in-process redaction floor scrubs secrets (provider keys, tokens, JWTs, PEM blocks, DB passwords, …), emails, and absolute home paths. "Raw" mode means *full turns*, not *leaked credentials* — the floor still runs. Tool calls ship only hashes, byte sizes, and allowlisted command verbs — never raw args, results, paths, or command text.
|
|
143
152
|
|
|
144
153
|
What the floor redacts: Anthropic / OpenAI / Google / AWS / GitHub / Slack / Stripe / Discord keys and tokens, JWTs, PEM private-key blocks, modelstat device secrets, generic `NAME_KEY=value` env secrets (the name is kept, the value is dropped), `Bearer` tokens, database-URL passwords, lone 40-char AWS-style secret blobs, email addresses, and absolute `/Users/…`, `/home/…`, and `C:\Users\…` paths.
|
|
145
154
|
|
|
146
|
-
## What's live today (v0.0.
|
|
155
|
+
## What's live today (v0.0.2)
|
|
147
156
|
|
|
148
157
|
Early release — the honest state, so nothing surprises you:
|
|
149
158
|
|
|
@@ -111,13 +111,22 @@ cfg = Config("msk_live_…", "raw_sdk_openai").with_remote(
|
|
|
111
111
|
)
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
## Taxonomy auto-detection (off by default)
|
|
115
|
+
|
|
116
|
+
modelstat can auto-detect a work-type *taxonomy* over your sessions, but that's tuned for interactive coding sessions — backend LLM usage usually isn't. So for the SDK taxonomy is **off by default**: every batch ships an explicit `auto_taxonomy: false`. Opt in with the config flag:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
cfg = Config("msk_live_…", "raw_sdk_openai")
|
|
120
|
+
cfg.auto_taxonomy = True # force server-side taxonomy auto-detection on
|
|
121
|
+
```
|
|
122
|
+
|
|
114
123
|
## Privacy floor (always on)
|
|
115
124
|
|
|
116
125
|
Before any bytes leave the SDK process — in **every** mode — an in-process redaction floor scrubs secrets (provider keys, tokens, JWTs, PEM blocks, DB passwords, …), emails, and absolute home paths. "Raw" mode means *full turns*, not *leaked credentials* — the floor still runs. Tool calls ship only hashes, byte sizes, and allowlisted command verbs — never raw args, results, paths, or command text.
|
|
117
126
|
|
|
118
127
|
What the floor redacts: Anthropic / OpenAI / Google / AWS / GitHub / Slack / Stripe / Discord keys and tokens, JWTs, PEM private-key blocks, modelstat device secrets, generic `NAME_KEY=value` env secrets (the name is kept, the value is dropped), `Bearer` tokens, database-URL passwords, lone 40-char AWS-style secret blobs, email addresses, and absolute `/Users/…`, `/home/…`, and `C:\Users\…` paths.
|
|
119
128
|
|
|
120
|
-
## What's live today (v0.0.
|
|
129
|
+
## What's live today (v0.0.2)
|
|
121
130
|
|
|
122
131
|
Early release — the honest state, so nothing surprises you:
|
|
123
132
|
|
|
@@ -46,7 +46,7 @@ from .config import DEFAULT_DAEMON_URL, Config, Mode, RedactionPolicy
|
|
|
46
46
|
from .redact import Redacted, redact
|
|
47
47
|
from .transport import FakeTransport, HttpTransport, Transport, TransportError
|
|
48
48
|
from .wire import (
|
|
49
|
-
|
|
49
|
+
PricingMode,
|
|
50
50
|
EventKind,
|
|
51
51
|
GitContext,
|
|
52
52
|
IngestBatch,
|
|
@@ -86,7 +86,7 @@ __all__ = [
|
|
|
86
86
|
"TokenUsage",
|
|
87
87
|
"GitContext",
|
|
88
88
|
"EventKind",
|
|
89
|
-
"
|
|
89
|
+
"PricingMode",
|
|
90
90
|
"ToolCallStatus",
|
|
91
91
|
"content_hash",
|
|
92
92
|
"source_event_id",
|
|
@@ -19,7 +19,7 @@ from . import wire
|
|
|
19
19
|
from .config import Config, RedactionPolicy
|
|
20
20
|
from .redact import redact
|
|
21
21
|
from .wire import (
|
|
22
|
-
|
|
22
|
+
PricingMode,
|
|
23
23
|
EventKind,
|
|
24
24
|
GitContext,
|
|
25
25
|
IngestBatch,
|
|
@@ -83,7 +83,7 @@ class LlmCall:
|
|
|
83
83
|
completion: Optional[str] = None
|
|
84
84
|
cwd: Optional[str] = None
|
|
85
85
|
git: Optional[GitContext] = None
|
|
86
|
-
|
|
86
|
+
pricing_mode: Optional[PricingMode] = None
|
|
87
87
|
tool_calls: List[ToolCallInput] = field(default_factory=list)
|
|
88
88
|
|
|
89
89
|
# ---- chainable builder helpers (ergonomic, mirror the Rust builder) -----
|
|
@@ -193,7 +193,7 @@ def _event_from_call(
|
|
|
193
193
|
cwd=call.cwd,
|
|
194
194
|
git=call.git,
|
|
195
195
|
duration_ms=call.duration_ms,
|
|
196
|
-
|
|
196
|
+
pricing_mode=call.pricing_mode,
|
|
197
197
|
content_excerpt=_build_excerpt(cfg, call),
|
|
198
198
|
)
|
|
199
199
|
|
|
@@ -260,5 +260,8 @@ def build_batch(
|
|
|
260
260
|
daemon_version=cfg.client_version,
|
|
261
261
|
events=events,
|
|
262
262
|
tool_calls=tool_calls,
|
|
263
|
+
# Always send an explicit value reflecting the config so backend usage is
|
|
264
|
+
# off-by-default but users can opt in.
|
|
265
|
+
auto_taxonomy=cfg.auto_taxonomy,
|
|
263
266
|
)
|
|
264
267
|
return batch, seq
|
|
@@ -108,6 +108,12 @@ class Config:
|
|
|
108
108
|
flush_interval: float = 2.0
|
|
109
109
|
# Flush eagerly once this many records are buffered.
|
|
110
110
|
flush_max_batch: int = 256
|
|
111
|
+
# Whether the server should run taxonomy auto-detection on batches from this
|
|
112
|
+
# client. Ships as the wire ``auto_taxonomy`` field. Defaults to ``False``
|
|
113
|
+
# for SDK/backend integrations -- backend LLM usage isn't interactive
|
|
114
|
+
# work-sessions, so taxonomy is **off by default**; set it to ``True`` to opt
|
|
115
|
+
# in.
|
|
116
|
+
auto_taxonomy: bool = False
|
|
111
117
|
|
|
112
118
|
def __post_init__(self) -> None:
|
|
113
119
|
# The wire field is constrained to 1..=40 chars; keep the SDK honest so
|
|
@@ -36,7 +36,7 @@ import blake3
|
|
|
36
36
|
__all__ = [
|
|
37
37
|
"TokenUsage",
|
|
38
38
|
"EventKind",
|
|
39
|
-
"
|
|
39
|
+
"PricingMode",
|
|
40
40
|
"ToolCallStatus",
|
|
41
41
|
"GitContext",
|
|
42
42
|
"RawEvent",
|
|
@@ -118,7 +118,7 @@ class EventKind(str, Enum):
|
|
|
118
118
|
SUMMARY = "summary"
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
class
|
|
121
|
+
class PricingMode(str, Enum):
|
|
122
122
|
"""How the provider billed the call."""
|
|
123
123
|
|
|
124
124
|
SUBSCRIPTION = "subscription"
|
|
@@ -181,7 +181,7 @@ class RawEvent:
|
|
|
181
181
|
cwd: Optional[str] = None
|
|
182
182
|
git: Optional[GitContext] = None
|
|
183
183
|
duration_ms: Optional[int] = None
|
|
184
|
-
|
|
184
|
+
pricing_mode: Optional[PricingMode] = None
|
|
185
185
|
# Redacted excerpt used to build summaries downstream. Capped at 320 chars
|
|
186
186
|
# in the standard (floor-redacted) path; carries the full redacted turns in
|
|
187
187
|
# remote-raw mode, where the server summarizes.
|
|
@@ -206,8 +206,8 @@ class RawEvent:
|
|
|
206
206
|
out["git"] = self.git.to_dict()
|
|
207
207
|
if self.duration_ms is not None:
|
|
208
208
|
out["duration_ms"] = self.duration_ms
|
|
209
|
-
if self.
|
|
210
|
-
out["
|
|
209
|
+
if self.pricing_mode is not None:
|
|
210
|
+
out["pricing_mode"] = self.pricing_mode.value
|
|
211
211
|
if self.content_excerpt is not None:
|
|
212
212
|
out["content_excerpt"] = self.content_excerpt
|
|
213
213
|
return out
|
|
@@ -292,6 +292,12 @@ class IngestBatch:
|
|
|
292
292
|
daemon_version: str
|
|
293
293
|
events: List[RawEvent] = field(default_factory=list)
|
|
294
294
|
tool_calls: List[ToolCallWire] = field(default_factory=list)
|
|
295
|
+
# Per-batch taxonomy auto-detection toggle. ``None`` = server default
|
|
296
|
+
# (taxonomy auto/on); ``False`` = skip taxonomy auto-detection for this
|
|
297
|
+
# batch; ``True`` = force it on. SDK/backend integrations default this to
|
|
298
|
+
# ``False`` (backend LLM usage isn't interactive work-sessions). Included in
|
|
299
|
+
# ``to_dict()`` only when not None.
|
|
300
|
+
auto_taxonomy: Optional[bool] = None
|
|
295
301
|
|
|
296
302
|
def to_dict(self) -> Dict[str, Any]:
|
|
297
303
|
out: Dict[str, Any] = {
|
|
@@ -303,6 +309,9 @@ class IngestBatch:
|
|
|
303
309
|
# Omit ``tool_calls`` entirely when empty (do NOT send an empty list).
|
|
304
310
|
if self.tool_calls:
|
|
305
311
|
out["tool_calls"] = [t.to_dict() for t in self.tool_calls]
|
|
312
|
+
# Optional key -- omit when None (never emit null).
|
|
313
|
+
if self.auto_taxonomy is not None:
|
|
314
|
+
out["auto_taxonomy"] = self.auto_taxonomy
|
|
306
315
|
return out
|
|
307
316
|
|
|
308
317
|
|
|
@@ -115,6 +115,19 @@ class TestBuildBatch(unittest.TestCase):
|
|
|
115
115
|
batch, _ = build_batch(cfg(), [call], 0)
|
|
116
116
|
self.assertNotIn("tool_calls", batch.to_dict())
|
|
117
117
|
|
|
118
|
+
def test_auto_taxonomy_defaults_off_and_opts_in(self) -> None:
|
|
119
|
+
# Default config: taxonomy off -> explicit ``auto_taxonomy: false``.
|
|
120
|
+
batch, _ = build_batch(cfg(), [LlmCall("openai", "sess_1")], 0)
|
|
121
|
+
self.assertEqual(batch.auto_taxonomy, False)
|
|
122
|
+
self.assertEqual(batch.to_dict()["auto_taxonomy"], False)
|
|
123
|
+
|
|
124
|
+
# Opt in: flag True -> wire ``auto_taxonomy: true``.
|
|
125
|
+
on = cfg()
|
|
126
|
+
on.auto_taxonomy = True
|
|
127
|
+
batch, _ = build_batch(on, [LlmCall("openai", "sess_1")], 0)
|
|
128
|
+
self.assertEqual(batch.auto_taxonomy, True)
|
|
129
|
+
self.assertEqual(batch.to_dict()["auto_taxonomy"], True)
|
|
130
|
+
|
|
118
131
|
def test_raw_mode_sends_full_untruncated_turns_still_floor_redacted(
|
|
119
132
|
self,
|
|
120
133
|
) -> None:
|
|
@@ -7,7 +7,7 @@ import unittest
|
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
8
|
|
|
9
9
|
from modelstat.wire import (
|
|
10
|
-
|
|
10
|
+
PricingMode,
|
|
11
11
|
EventKind,
|
|
12
12
|
RawEvent,
|
|
13
13
|
TokenUsage,
|
|
@@ -57,13 +57,13 @@ class TestSerialization(unittest.TestCase):
|
|
|
57
57
|
tokens=TokenUsage(input=10, output=5),
|
|
58
58
|
model="gpt-x",
|
|
59
59
|
duration_ms=1200,
|
|
60
|
-
|
|
60
|
+
pricing_mode=PricingMode.API,
|
|
61
61
|
content_excerpt="hello",
|
|
62
62
|
)
|
|
63
63
|
j = ev.to_dict()
|
|
64
64
|
self.assertEqual(j["kind"], "assistant_message")
|
|
65
65
|
self.assertEqual(j["agent"], "raw_sdk_openai")
|
|
66
|
-
self.assertEqual(j["
|
|
66
|
+
self.assertEqual(j["pricing_mode"], "api")
|
|
67
67
|
self.assertEqual(j["tokens"]["input"], 10)
|
|
68
68
|
# Tokens object always carries all five classes.
|
|
69
69
|
self.assertEqual(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|