scopecall-py 0.2.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.
Files changed (31) hide show
  1. scopecall_py-0.2.0/.gitignore +1 -0
  2. scopecall_py-0.2.0/LICENSE +121 -0
  3. scopecall_py-0.2.0/PKG-INFO +521 -0
  4. scopecall_py-0.2.0/README.md +482 -0
  5. scopecall_py-0.2.0/examples/fastapi/README.md +61 -0
  6. scopecall_py-0.2.0/examples/fastapi/app.py +200 -0
  7. scopecall_py-0.2.0/pyproject.toml +73 -0
  8. scopecall_py-0.2.0/scopecall/__init__.py +81 -0
  9. scopecall_py-0.2.0/scopecall/_config.py +129 -0
  10. scopecall_py-0.2.0/scopecall/_context.py +131 -0
  11. scopecall_py-0.2.0/scopecall/_exporter.py +273 -0
  12. scopecall_py-0.2.0/scopecall/_pricing.py +69 -0
  13. scopecall_py-0.2.0/scopecall/_redactor.py +80 -0
  14. scopecall_py-0.2.0/scopecall/_sdk.py +518 -0
  15. scopecall_py-0.2.0/scopecall/_version.py +7 -0
  16. scopecall_py-0.2.0/scopecall/instrumentation/__init__.py +13 -0
  17. scopecall_py-0.2.0/scopecall/instrumentation/_anthropic.py +517 -0
  18. scopecall_py-0.2.0/scopecall/instrumentation/_common.py +243 -0
  19. scopecall_py-0.2.0/scopecall/instrumentation/_openai.py +528 -0
  20. scopecall_py-0.2.0/scopecall/transport/__init__.py +0 -0
  21. scopecall_py-0.2.0/scopecall/wire/__init__.py +5 -0
  22. scopecall_py-0.2.0/scopecall/wire/_event.py +171 -0
  23. scopecall_py-0.2.0/tests/conftest.py +36 -0
  24. scopecall_py-0.2.0/tests/test_anthropic_instrumentation.py +518 -0
  25. scopecall_py-0.2.0/tests/test_config.py +115 -0
  26. scopecall_py-0.2.0/tests/test_context.py +144 -0
  27. scopecall_py-0.2.0/tests/test_event.py +141 -0
  28. scopecall_py-0.2.0/tests/test_openai_instrumentation.py +479 -0
  29. scopecall_py-0.2.0/tests/test_redaction.py +212 -0
  30. scopecall_py-0.2.0/tests/test_streaming_context.py +245 -0
  31. scopecall_py-0.2.0/tests/test_workflow_emission.py +206 -0
@@ -0,0 +1 @@
1
+ .venv/
@@ -0,0 +1,121 @@
1
+ Business Source License 1.1
2
+
3
+ Licensor: ScopeCall Inc.
4
+ Licensed Work: ScopeCall
5
+ The Licensed Work is (c) 2026 ScopeCall Inc.
6
+ Additional Use Grant: You may make production use of the Licensed Work, provided
7
+ such use does not include offering the Licensed Work to
8
+ third parties on a hosted or embedded basis in order to
9
+ compete with ScopeCall's paid service offerings.
10
+ Change Date: May 26, 2031
11
+ Change License: Apache License, Version 2.0
12
+
13
+ For information about alternative licensing arrangements for the Licensed Work,
14
+ please contact: legal@scopecall.com
15
+
16
+ ---
17
+
18
+ Business Source License 1.1
19
+
20
+ Parameters
21
+
22
+ Licensor: ScopeCall Inc.
23
+
24
+ Licensed Work: ScopeCall. The Licensed Work is (c) 2026 ScopeCall Inc.
25
+
26
+ Additional Use Grant: You may make production use of the Licensed Work, provided
27
+ such use does not include offering the Licensed Work to third parties on a hosted
28
+ or embedded basis in order to compete with ScopeCall's paid service offerings.
29
+
30
+ For the avoidance of doubt, you may:
31
+ - Self-host ScopeCall for your own organization's internal use.
32
+ - Modify and contribute back to the Licensed Work.
33
+ - Build applications that send data to a self-hosted ScopeCall instance.
34
+
35
+ You may not:
36
+ - Offer ScopeCall as a managed service to third parties.
37
+ - White-label ScopeCall and resell it as your own product.
38
+
39
+ Change Date: May 26, 2031
40
+
41
+ Change License: Apache License, Version 2.0
42
+
43
+ ---
44
+
45
+ License text
46
+
47
+ Business Source License 1.1
48
+
49
+ "Licensed Work" means the software made available by the Licensor under this
50
+ License.
51
+
52
+ "Licensor" means the licensor of the Licensed Work.
53
+
54
+ "Additional Use Grant" means the additional use grant defined in the Parameters.
55
+
56
+ "Change Date" means the date specified in the Parameters.
57
+
58
+ "Change License" means the license specified in the Parameters.
59
+
60
+ On the Change Date, or the fourth anniversary of the first publicly available
61
+ distribution of a specific version of the Licensed Work under this License,
62
+ whichever comes first, the Licensor hereby grants you rights under the terms of
63
+ the Change License, and the rights granted in the paragraph below terminate.
64
+
65
+ Subject to the Additional Use Grant, from the first day you receive a copy of
66
+ the Licensed Work under this License, through the Change Date, the Licensor
67
+ grants you a non-exclusive, worldwide, non-sublicensable, non-transferable,
68
+ royalty-free limited license to copy, modify, display, perform, and distribute
69
+ the Licensed Work for the purpose of your internal, non-commercial use.
70
+
71
+ Notice: If your use of the Licensed Work does not comply with the requirements
72
+ currently in effect as described in this License, you must purchase a commercial
73
+ license from the Licensor, its affiliated entities, or authorized resellers, or
74
+ you must refrain from using the Licensed Work.
75
+
76
+ All copies of the original and modified Licensed Work, and derivative works of
77
+ the Licensed Work, are subject to this License. This License applies separately
78
+ for each version of the Licensed Work, and the Change Date may vary for each
79
+ version of the Licensed Work released by Licensor.
80
+
81
+ You must conspicuously display this License on each original or modified copy of
82
+ the Licensed Work. If you receive the Licensed Work in original or modified form
83
+ from a third party, the terms and conditions set forth in this License apply to
84
+ your use of that work.
85
+
86
+ Any use of the Licensed Work in violation of this License will automatically
87
+ terminate your rights under this License for the current and all other versions
88
+ of the Licensed Work.
89
+
90
+ This License does not grant you any right in any trademark or logo of Licensor
91
+ or its affiliates (provided that you may use a trademark or logo of Licensor as
92
+ expressly required by this License).
93
+
94
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
95
+ "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS
96
+ OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS
97
+ FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
98
+
99
+ MariaDB hereby grants you permission to use this License's text to license your
100
+ works, and to refer to it using the trademark "Business Source License", as long
101
+ as you comply with the Covenants of Licensor below.
102
+
103
+ Covenants of Licensor
104
+
105
+ In consideration of the right to use this License's text and the "Business Source
106
+ License" name and trademark, Licensor covenants to MariaDB, and to all who
107
+ receive notice of this covenant:
108
+
109
+ 1. To specify as the Change License the GPL Version 2.0 or any later version, or
110
+ a license that is compatible with GPL Version 2.0 or a later version, where
111
+ "compatible" means that software provided under the Change License can be
112
+ included in a program with software provided under GPL Version 2.0 or a later
113
+ version. Licensor may specify additional Change Licenses without limitation.
114
+
115
+ 2. To either: (a) specify an additional grant of rights to use that does not
116
+ impose any additional restriction on the right granted in this License, as the
117
+ Additional Use Grant; or (b) insert the text "None".
118
+
119
+ 3. To specify a Change Date.
120
+
121
+ 4. Not to modify this License in any other way.
@@ -0,0 +1,521 @@
1
+ Metadata-Version: 2.4
2
+ Name: scopecall-py
3
+ Version: 0.2.0
4
+ Summary: Source-available, self-hostable AI observability — scope every LLM call in production
5
+ Project-URL: Homepage, https://scopecall.com
6
+ Project-URL: Repository, https://github.com/scopecall/scopecall
7
+ Project-URL: Documentation, https://docs.scopecall.com
8
+ Project-URL: Issues, https://github.com/scopecall/scopecall/issues
9
+ Author-email: ScopeCall <founders@scopecall.com>
10
+ License: BUSL-1.1
11
+ License-File: LICENSE
12
+ Keywords: ai,anthropic,cost,llm,observability,openai,scopecall,tracing
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: System :: Monitoring
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.24.0
24
+ Requires-Dist: typing-extensions>=4.0.0; python_version < '3.11'
25
+ Provides-Extra: all
26
+ Requires-Dist: anthropic>=0.20.0; extra == 'all'
27
+ Requires-Dist: openai>=1.0.0; extra == 'all'
28
+ Provides-Extra: anthropic
29
+ Requires-Dist: anthropic>=0.20.0; extra == 'anthropic'
30
+ Provides-Extra: dev
31
+ Requires-Dist: httpx>=0.24.0; extra == 'dev'
32
+ Requires-Dist: mypy>=1.0; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
34
+ Requires-Dist: pytest>=7.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
36
+ Provides-Extra: openai
37
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # scopecall
41
+
42
+ Python SDK for [ScopeCall](https://scopecall.com) — source-available, self-hostable AI cost and workflow observability.
43
+
44
+ [![PyPI](https://img.shields.io/pypi/v/scopecall-py)](https://pypi.org/project/scopecall-py/)
45
+ [![License](https://img.shields.io/badge/license-BUSL--1.1-blue)](LICENSE)
46
+ [![Python](https://img.shields.io/pypi/pyversions/scopecall-py)](https://pypi.org/project/scopecall-py/)
47
+
48
+ Wraps the OpenAI and Anthropic Python clients so every LLM call shows
49
+ up in your ScopeCall dashboard with cost, latency, prompt-version, and
50
+ workflow-tree attribution — **without** routing traffic through a
51
+ proxy.
52
+
53
+ ---
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install scopecall-py
59
+
60
+ # Or with provider extras (recommended — pins to a known-good lower bound):
61
+ pip install "scopecall-py[openai]"
62
+ pip install "scopecall-py[anthropic]"
63
+ pip install "scopecall-py[all]"
64
+ ```
65
+
66
+ The PyPI package is named `scopecall-py` (Supabase-style language
67
+ suffix); the Python import name stays just `scopecall`. So you `pip
68
+ install scopecall-py` and then `from scopecall import init`.
69
+
70
+ Python 3.10+ required.
71
+
72
+ ---
73
+
74
+ ## Quick start
75
+
76
+ ```python
77
+ import scopecall
78
+ from openai import OpenAI
79
+
80
+ # Initialize once at app startup.
81
+ sdk = scopecall.init(
82
+ api_key="sc_live_xxx", # from your ScopeCall dashboard
83
+ endpoint="http://localhost:8080/v1/ingest", # required: self-hosted ingest URL
84
+ )
85
+
86
+ # Wrap the OpenAI client — every chat.completions.create call is now traced.
87
+ openai_client = sdk.instrument(OpenAI())
88
+
89
+ with sdk.trace("support-agent", user_id="user_123") as ctx:
90
+ response = openai_client.chat.completions.create(
91
+ model="gpt-4o-mini",
92
+ messages=[{"role": "user", "content": "Hello"}],
93
+ )
94
+
95
+ # Traces appear in your dashboard within seconds.
96
+ ```
97
+
98
+ > **No hosted-Cloud default yet.** A managed default endpoint will
99
+ > return when ScopeCall Cloud is live. Until then, `init()` requires
100
+ > `endpoint` to be set explicitly when using `api_key` — fail-fast is
101
+ > safer than silently sending events to a domain that doesn't exist.
102
+
103
+ ---
104
+
105
+ ## Configuration
106
+
107
+ ```python
108
+ sdk = scopecall.init(
109
+ api_key="sc_live_xxx", # required (or use debug=True / output=<path>)
110
+ endpoint="http://localhost:8080/v1/ingest", # required when using api_key
111
+ environment="production", # optional; defaults to "production"
112
+ capture_content=True, # optional; record prompts/completions (default True)
113
+ redact_pii=True, # optional; PII redaction (default True)
114
+ batch_size=50, # optional; events per HTTP batch
115
+ max_retries=3, # optional; retry attempts on transient failure
116
+ flush_interval=5.0, # optional; seconds between auto-flush
117
+ debug=False, # optional; route events to stdout instead of HTTP
118
+ )
119
+ ```
120
+
121
+ Other transport modes:
122
+
123
+ ```python
124
+ # Console mode — pretty-prints events to stdout. Useful during integration.
125
+ sdk = scopecall.init(debug=True)
126
+
127
+ # File mode — appends NDJSON events to a path. Useful for offline capture.
128
+ sdk = scopecall.init(output="/var/log/scopecall.ndjson")
129
+
130
+ # Disabled mode — no-op SDK that swallows every call. Useful in tests.
131
+ sdk = scopecall.init(disabled=True)
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Anthropic
137
+
138
+ ```python
139
+ import scopecall
140
+ import anthropic
141
+
142
+ sdk = scopecall.init(
143
+ api_key="sc_live_xxx",
144
+ endpoint="http://localhost:8080/v1/ingest",
145
+ )
146
+
147
+ anthropic_client = sdk.instrument(anthropic.Anthropic(), provider="anthropic")
148
+
149
+ msg = anthropic_client.messages.create(
150
+ model="claude-3-5-sonnet-20241022",
151
+ max_tokens=1024,
152
+ messages=[{"role": "user", "content": "Hello"}],
153
+ )
154
+ ```
155
+
156
+ Streaming works the same way — pass `stream=True` and iterate. TTFT
157
+ (time to first token) is captured automatically; output content is
158
+ assembled from `content_block_delta` events; final token counts come
159
+ from the `message_delta` event Anthropic emits near end-of-stream.
160
+
161
+ ---
162
+
163
+ ## Async
164
+
165
+ Both `AsyncOpenAI` and `AsyncAnthropic` are first-class — `instrument()`
166
+ auto-detects async vs sync from the client and wraps accordingly. No
167
+ separate API.
168
+
169
+ ```python
170
+ import asyncio
171
+ import scopecall
172
+ from openai import AsyncOpenAI
173
+
174
+ sdk = scopecall.init(
175
+ api_key="sc_live_xxx",
176
+ endpoint="http://localhost:8080/v1/ingest",
177
+ )
178
+ client = sdk.instrument(AsyncOpenAI())
179
+
180
+ async def main():
181
+ # Use asyncio.gather so this snippet runs on Python 3.10 (the SDK's
182
+ # lower bound). asyncio.TaskGroup is 3.11+; if you're on 3.11 or
183
+ # later it's a cleaner choice for structured concurrency.
184
+ await asyncio.gather(*(
185
+ client.chat.completions.create(
186
+ model="gpt-4o-mini",
187
+ messages=[{"role": "user", "content": f"Hello {i}"}],
188
+ )
189
+ for i in range(3)
190
+ ))
191
+
192
+ asyncio.run(main())
193
+ ```
194
+
195
+ `contextvars` propagate the active `sdk.trace()` context across
196
+ `await` and `asyncio.create_task()`, so concurrent calls inside the
197
+ same trace get the right `parent_span_id` automatically.
198
+
199
+ ---
200
+
201
+ ## Workflow tracing
202
+
203
+ The `sdk.trace(name)` block emits a synthetic **workflow span** when it
204
+ exits, so the ScopeCall dashboard can render the parent → child
205
+ structure of multi-call agents:
206
+
207
+ ```python
208
+ with sdk.trace("rag-question", user_id=user_id, session_id=session_id):
209
+ # 1) retrieve documents (could itself be an LLM call)
210
+ docs = retriever.retrieve(question)
211
+
212
+ # 2) call the LLM with the retrieved context
213
+ response = openai_client.chat.completions.create(
214
+ model="gpt-4o-mini",
215
+ messages=[
216
+ {"role": "system", "content": f"Context:\n{docs}"},
217
+ {"role": "user", "content": question},
218
+ ],
219
+ )
220
+ ```
221
+
222
+ In the dashboard's trace tree, that block renders as:
223
+
224
+ ```
225
+ rag-question (workflow span)
226
+ └── chat.completions.create (LLM span)
227
+ ```
228
+
229
+ Nested traces work too — the inner block inherits `trace_id`,
230
+ gets its own `span_id`, and sets `parent_span_id` to the outer block.
231
+
232
+ ### Streaming + workflow latency
233
+
234
+ When a streaming response is iterated AFTER the enclosing
235
+ `sdk.trace()` block has exited (the common pattern with FastAPI's
236
+ `StreamingResponse`, where the route handler returns and the iterator
237
+ runs later), the SDK still attaches the child LLM event to the
238
+ workflow span correctly — context is snapshotted when
239
+ `.create()` is called, not when the stream is consumed.
240
+
241
+ But the workflow span's **latency** only covers what's inside the
242
+ `with` block. If you want workflow latency to reflect the full
243
+ streaming duration, keep the trace block open across the iteration:
244
+
245
+ ```python
246
+ async def event_source():
247
+ with sdk.trace("chat-api", user_id=req.user_id):
248
+ stream = await openai_client.chat.completions.create(
249
+ model="gpt-4o-mini",
250
+ messages=messages,
251
+ stream=True,
252
+ )
253
+ async for chunk in stream:
254
+ yield chunk
255
+
256
+ return StreamingResponse(event_source(), media_type="text/event-stream")
257
+ ```
258
+
259
+ The runnable FastAPI example below uses exactly this shape.
260
+
261
+ ---
262
+
263
+ ## Per-call metadata
264
+
265
+ Set defaults SDK-wide on `init()`, then override per-trace:
266
+
267
+ ```python
268
+ sdk = scopecall.init(
269
+ api_key="sc_live_xxx",
270
+ endpoint="http://localhost:8080/v1/ingest",
271
+ default_feature="chat", # every call tagged "chat"
272
+ default_user_id="anonymous",
273
+ default_prompt_version=os.getenv("DEPLOY_SHA"), # auto-tag with commit hash
274
+ )
275
+
276
+ # Per-call overrides win over defaults; nested-trace inheritance fills
277
+ # the gap for prompt_version (trace > parent > default > None).
278
+ with sdk.trace("billing-agent", user_id=user.id, prompt_version="refund-v3"):
279
+ ...
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Prompt-version tracking
285
+
286
+ Tag each `sdk.trace()` with a `prompt_version`. The ScopeCall Prompts
287
+ page surfaces cost / latency / error-rate **per version** — ship a new
288
+ prompt, see whether output tokens went up:
289
+
290
+ ```python
291
+ PROMPT_V = "refund-policy-v7"
292
+
293
+ with sdk.trace("support-agent", prompt_version=PROMPT_V):
294
+ response = openai_client.chat.completions.create(
295
+ model="gpt-4o",
296
+ messages=[
297
+ {"role": "system", "content": PROMPT_V_TEXT},
298
+ {"role": "user", "content": question},
299
+ ],
300
+ )
301
+ ```
302
+
303
+ Nested traces inherit the parent's `prompt_version`. To clear it on a
304
+ child span, pass `prompt_version=None` explicitly (which doesn't
305
+ override; you'd want a different scope name instead).
306
+
307
+ ---
308
+
309
+ ## Manual instrumentation (LangChain, LlamaIndex, custom)
310
+
311
+ If you're calling an LLM through a framework that wraps the underlying
312
+ client (LangChain, LlamaIndex, CrewAI, your own gateway), `instrument()`
313
+ can't see through to the raw call. Use `sdk.record_llm_call()` to emit
314
+ events manually — same wire format, same trace-context chaining:
315
+
316
+ ```python
317
+ with sdk.trace("rag-answer"):
318
+ docs = retriever.retrieve(q) # your code, not instrumented
319
+
320
+ # ... call your custom LLM wrapper ...
321
+ sdk.record_llm_call(
322
+ model="gpt-4o-mini",
323
+ provider="openai",
324
+ input_tokens=1234,
325
+ output_tokens=567,
326
+ latency_ms=842,
327
+ input_text=prompt,
328
+ output_text=answer,
329
+ finish_reason="stop",
330
+ )
331
+ ```
332
+
333
+ `record_llm_call` reads the current `sdk.trace()` context to set
334
+ `parent_span_id` and inherit feature / user / session / prompt_version.
335
+ PII redaction (`redact_pii=True`) applies to manual calls too — input
336
+ and output run through the same scrubber the auto-instrumented path
337
+ uses.
338
+
339
+ For deeper sub-step instrumentation (e.g. "retrieve" and "rerank" as
340
+ separate visible spans), nest `sdk.trace()` blocks rather than reaching
341
+ for a sub-span helper. Each nested `trace` block emits its own
342
+ workflow span and chains correctly:
343
+
344
+ ```python
345
+ with sdk.trace("rag-answer"):
346
+ with sdk.trace("retrieve"):
347
+ docs = retriever.retrieve(q)
348
+ with sdk.trace("generate"):
349
+ sdk.record_llm_call(...)
350
+ ```
351
+
352
+ ---
353
+
354
+ ## FastAPI
355
+
356
+ ```python
357
+ from contextlib import asynccontextmanager
358
+
359
+ import scopecall
360
+ from fastapi import FastAPI
361
+ from openai import AsyncOpenAI
362
+
363
+ sdk: scopecall.ScopeCallSDK
364
+ client: AsyncOpenAI
365
+
366
+
367
+ @asynccontextmanager
368
+ async def lifespan(app: FastAPI):
369
+ """Initialize the SDK once at startup; close on shutdown so the
370
+ background flush thread drains pending events before exit."""
371
+ global sdk, client
372
+ sdk = scopecall.init(
373
+ api_key=os.environ["SCOPECALL_API_KEY"],
374
+ endpoint=os.environ.get(
375
+ "SCOPECALL_ENDPOINT", "http://localhost:8080/v1/ingest"
376
+ ),
377
+ environment=os.environ.get("ENV", "production"),
378
+ default_prompt_version=os.environ.get("DEPLOY_SHA"),
379
+ )
380
+ client = sdk.instrument(AsyncOpenAI())
381
+ yield
382
+ sdk.close(timeout=5.0)
383
+
384
+
385
+ app = FastAPI(lifespan=lifespan)
386
+
387
+
388
+ @app.post("/chat")
389
+ async def chat(req: ChatRequest):
390
+ with sdk.trace("chat-api", user_id=req.user_id, session_id=req.session_id):
391
+ response = await client.chat.completions.create(
392
+ model="gpt-4o-mini",
393
+ messages=req.messages,
394
+ )
395
+ return {"reply": response.choices[0].message.content}
396
+ ```
397
+
398
+ A runnable version of this example lives in
399
+ [`examples/fastapi/`](examples/fastapi/).
400
+
401
+ ---
402
+
403
+ ## What gets captured
404
+
405
+ Every traced LLM call captures:
406
+
407
+ | Field | Description |
408
+ |-------|-------------|
409
+ | `model` | Canonical model name (e.g. `gpt-4o-mini`, `claude-3-5-sonnet-20241022`) |
410
+ | `provider` | `openai` or `anthropic` |
411
+ | `input_tokens` | Prompt token count |
412
+ | `output_tokens` | Completion token count |
413
+ | `cache_read_tokens` | OpenAI prompt cache hits / Anthropic `cache_read_input_tokens` |
414
+ | `cost_usd` | Computed server-side from the bundled pricing table |
415
+ | `latency_ms` | End-to-end latency |
416
+ | `ttft_ms` | Time to first token (streaming only) |
417
+ | `finish_reason` | `stop` / `length` / `tool_calls` / `end_turn` (Anthropic) |
418
+ | `status` | `success` / `error` / `timeout` / `rate_limited` |
419
+ | `error_message` | Error detail on failure |
420
+ | `input_text` | Full prompt (redacted per your PII config) |
421
+ | `output_text` | Full completion |
422
+ | `tool_calls` | Tool-use blocks as JSON (Anthropic) |
423
+ | `prompt_version` | Per-trace label from `sdk.trace()` or config — powers the Prompts page |
424
+ | `feature_name` / `user_id` / `session_id` | From `sdk.trace()` or `init()` defaults |
425
+ | `kind` | `llm` for provider calls, `workflow` for `sdk.trace()` blocks |
426
+
427
+ ---
428
+
429
+ ## PII redaction
430
+
431
+ When `redact_pii=True` (the default), `input_text` and `output_text`
432
+ pass through a regex-based scrubber before leaving the process. The
433
+ same scrubber runs on auto-instrumented `chat.completions.create` /
434
+ `messages.create` calls AND on manual `sdk.record_llm_call(...)` —
435
+ the policy is the same regardless of how the event was generated.
436
+
437
+ | Pattern | Replacement |
438
+ |---|---|
439
+ | Email | `[EMAIL]` |
440
+ | Credit card (Luhn-validated) | `[CARD]` |
441
+ | SSN | `[SSN]` |
442
+ | IPv4 | `[IP]` |
443
+ | Phone | `[PHONE]` |
444
+
445
+ Add custom patterns via the public helper on the SDK:
446
+
447
+ ```python
448
+ sdk.add_redaction_pattern(
449
+ "UUID",
450
+ r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b",
451
+ )
452
+ ```
453
+
454
+ To disable redaction entirely (rarely a good idea outside dev), pass
455
+ `redact_pii=False`.
456
+
457
+ ---
458
+
459
+ ## Providers
460
+
461
+ | Provider | Status |
462
+ |----------|--------|
463
+ | OpenAI (`chat.completions.create`) — sync + async + streaming | ✅ v0.2.0 |
464
+ | Anthropic (`messages.create`) — sync + async + streaming | ✅ v0.2.0 |
465
+ | Google Gemini | 🔜 v0.3 |
466
+ | LangChain (via manual API today; native bridge planned) | 🔜 v0.3 |
467
+ | LlamaIndex (via manual API today) | 🔜 v0.3 |
468
+
469
+ For unsupported providers / frameworks, use `sdk.record_llm_call(...)`
470
+ to emit events directly — the wire format is the same.
471
+
472
+ ---
473
+
474
+ ## Migrating from `scopecall` v0.1.x
475
+
476
+ v0.1 used module-level globals (`scopecall.init()` then
477
+ `scopecall.trace(...)`). v0.2 returns an instance from `init()`.
478
+
479
+ The two changes most likely to break callers:
480
+
481
+ ```python
482
+ # v0.1 (old)
483
+ scopecall.init(api_key="...") # module-level
484
+ with scopecall.trace(feature="x"):
485
+ ...
486
+
487
+ # v0.2 (new)
488
+ sdk = scopecall.init(api_key="...", # endpoint REQUIRED now
489
+ endpoint="http://localhost:8080/v1/ingest")
490
+ with sdk.trace("x"): # name is positional
491
+ ...
492
+ ```
493
+
494
+ Other notable changes:
495
+
496
+ - `endpoint` is required when `api_key` is set (no silent default to
497
+ `https://ingest.scopecall.com` because Cloud isn't live yet).
498
+ - Removed dependency on Traceloop / OpenLLMetry.
499
+ - Native OpenAI + Anthropic instrumentation (sync + async + streaming)
500
+ via `sdk.instrument(client)`.
501
+ - New manual API: `sdk.record_llm_call(...)` and `sdk.add_redaction_pattern(name, regex)`.
502
+ - `LLMEvent` wire format adds `kind`, `prompt_version`,
503
+ `input_cost_usd`, `output_cost_usd`, `finish_reason`,
504
+ `cache_read_tokens`, `tool_calls`, and others to match the TS SDK
505
+ parity contract.
506
+
507
+ ---
508
+
509
+ ## Self-hosted setup
510
+
511
+ See the [main repo README](https://github.com/scopecall/scopecall) for
512
+ the full Docker Compose quickstart that brings up the Rust ingest, Rust
513
+ processor, ClickHouse, Postgres, Redpanda, Go API, and Next.js
514
+ dashboard.
515
+
516
+ ---
517
+
518
+ ## License
519
+
520
+ [BUSL-1.1](LICENSE) — free for any internal use; not for resale as a
521
+ managed service. Converts to Apache 2.0 on May 26, 2031.