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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelstat-sdk
3
- Version: 0.0.1
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.1)
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.1)
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
- BillingMode,
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
- "BillingMode",
89
+ "PricingMode",
90
90
  "ToolCallStatus",
91
91
  "content_hash",
92
92
  "source_event_id",
@@ -5,4 +5,4 @@ Read both at runtime (to build ``Config.client_version`` -> the wire
5
5
  ``[tool.hatch.version]``), so the two can never drift.
6
6
  """
7
7
 
8
- __version__ = "0.0.1"
8
+ __version__ = "0.0.2"
@@ -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
- BillingMode,
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
- billing: Optional[BillingMode] = None
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
- billing=call.billing,
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
- "BillingMode",
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 BillingMode(str, Enum):
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
- billing: Optional[BillingMode] = None
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.billing is not None:
210
- out["billing"] = self.billing.value
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
- BillingMode,
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
- billing=BillingMode.API,
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["billing"], "api")
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