coding-sh 0.1.3__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 coding.sh contributors
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 LICENSE README.md
2
+ prune tests
3
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: coding-sh
3
+ Version: 0.1.3
4
+ Summary: Trace any AI agent or LLM app from Python: send model calls, traces and tool spans to a coding.sh server.
5
+ Author: coding.sh contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://coding.sh
8
+ Project-URL: Repository, https://github.com/jackulau/coding.sh
9
+ Project-URL: Source, https://github.com/jackulau/coding.sh/tree/main/packages/sdk-python
10
+ Project-URL: Issues, https://github.com/jackulau/coding.sh/issues
11
+ Project-URL: Changelog, https://github.com/jackulau/coding.sh/blob/main/CHANGELOG.md
12
+ Keywords: ai agents,observability,evals,opentelemetry,llm,claude code,codex,openai,anthropic,tracing
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: System :: Monitoring
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: openai
23
+ Requires-Dist: openai>=1.0; extra == "openai"
24
+ Provides-Extra: anthropic
25
+ Requires-Dist: anthropic>=0.30; extra == "anthropic"
26
+ Provides-Extra: otel
27
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
28
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20; extra == "otel"
29
+ Dynamic: license-file
30
+
31
+ # coding-sh (Python SDK)
32
+
33
+ Send LLM calls, traces and tool spans from any Python service to a
34
+ [coding.sh](https://coding.sh) server (`coding.sh serve`). Stdlib only, Python 3.9+.
35
+
36
+ ```sh
37
+ pip install coding-sh
38
+ export CODING_SH_URL=https://obs.example.com CODING_SH_INGEST_KEY=csh_ik_...
39
+ ```
40
+
41
+ ```python
42
+ import coding_sh
43
+ from openai import OpenAI
44
+
45
+ client = coding_sh.observe(OpenAI()) # also AsyncOpenAI, Anthropic, AsyncAnthropic
46
+ ```
47
+
48
+ That's it: every `chat.completions.create`, `responses.create` and `messages.create` /
49
+ `messages.stream` call (sync, async and streaming) becomes an `llm.call` with tokens, model,
50
+ latency, time to first token, stop reason and tool calls. The server prices it.
51
+
52
+ Without `CODING_SH_URL` the SDK does nothing. It never changes return values, errors or stream
53
+ semantics, and never raises into your app.
54
+
55
+ ## Traces, tools and conversations
56
+
57
+ ```python
58
+ with coding_sh.trace("support-reply", session_id=conversation_id, user_id=user.id, input=question) as t:
59
+ with coding_sh.span("tool", "lookup_order", input={"id": 42}) as s:
60
+ s.set_output(orders.get(42))
61
+ reply = client.chat.completions.create(model="gpt-4o-mini", messages=msgs)
62
+ t.set_output(reply.choices[0].message.content)
63
+ ```
64
+
65
+ - Each top-level `trace()` (or untraced call) is its own session. Pass `session_id` (or wrap code
66
+ in `coding_sh.with_session(id)`) to make each trace a turn of one conversation.
67
+ - `trace` and `span` are context managers (`with` / `async with`) and decorators. A decorated
68
+ `span("tool", ...)` records the call's arguments as input and its return value as output.
69
+ - `span("step", name)` records a generic step.
70
+ - Context follows `contextvars`, so it crosses `await` and asyncio tasks. Work handed to a thread
71
+ pool starts untraced unless you run it with `contextvars.copy_context().run`.
72
+
73
+ ## LLM gateways
74
+
75
+ Point the client at a gateway and each call records which one it went through, and the request id
76
+ the gateway's lookup API takes, so `coding.sh gateway enrich` can add the billed cost and
77
+ `coding.sh sessions --flag gateway:<name>` finds the sessions. Detected: Vercel AI Gateway,
78
+ OpenRouter, LiteLLM, Cloudflare AI Gateway, Portkey, Helicone, Requesty, Ramp Router (router.com),
79
+ TrueFoundry, Kong, Bifrost and Martian, by base URL (a self-hosted gateway by its URL env var, e.g.
80
+ `LITELLM_PROXY_API_BASE`), by response header (`cf-aig-log-id`, `helicone-id`, `x-litellm-call-id`,
81
+ `x-portkey-trace-id`, …) or by response id (`gen_…`, `gen-…`, `rqsty-…`).
82
+ `observe()` adds a response hook to the client's HTTP client that reads headers only, and only for
83
+ calls it records. [Details](https://github.com/jackulau/coding.sh/blob/main/docs/integrations/ai-gateway.md#from-the-sdks).
84
+
85
+ ```python
86
+ client = coding_sh.observe(OpenAI(base_url="https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/compat"))
87
+ ```
88
+
89
+ ## Delivery
90
+
91
+ One daemon thread sends batches (up to 500 events or 4 MiB, gzip) to `POST /v1/ingest`. It retries
92
+ 429, 5xx and network errors (honoring `Retry-After`, else 1, 2, 4 ... s, at most 5 attempts) with
93
+ stable event ids, so retries never duplicate. 400/401/403 drop the batch with one warning. A full
94
+ queue drops new events and counts them. An `atexit` flush is bounded to 2 s.
95
+
96
+ ```python
97
+ coding_sh.flush(timeout=5) # e.g. at the end of a script, a job or a serverless handler
98
+ coding_sh.shutdown() # flush, then stop
99
+ coding_sh.stats() # {"queued", "sent", "dropped", "failed"}
100
+ ```
101
+
102
+ ## Config
103
+
104
+ `coding_sh.init(url=, key=, service=, capture=, flush_interval_ms=1000, max_batch=500, max_queue=10000, disabled=, debug=)`
105
+ is optional; arguments override env vars.
106
+
107
+ | Env | Meaning |
108
+ |---|---|
109
+ | `CODING_SH_URL` | Server base URL. Unset = SDK off. |
110
+ | `CODING_SH_INGEST_KEY` | `csh_ik_...` ingest key. It decides the project. |
111
+ | `CODING_SH_SERVICE` (or `OTEL_SERVICE_NAME`) | Service name; becomes the session's agent id. |
112
+ | `CODING_SH_CAPTURE_MODE` | `full` (default) or `metadata` (no message text or tool I/O). |
113
+ | `CODING_SH_DISABLED=1` | Turn the SDK off. |
114
+ | `CODING_SH_DEBUG=1` | Log transport details (never keys or bodies). |
115
+
116
+ ## OpenTelemetry
117
+
118
+ Already emitting gen_ai spans? Point OTel at the server instead (no SDK needed), or let the
119
+ helper do the setup:
120
+
121
+ ```python
122
+ from coding_sh.otel import configure, otel_env
123
+ configure(service="checkout-api") # TracerProvider + OTLP/HTTP exporter + Bearer key
124
+ env = otel_env() # OTEL_EXPORTER_OTLP_* vars for a subprocess
125
+ ```
126
+
127
+ `pip install 'coding-sh[otel]'`. `configure()` uses the standard `OTEL_EXPORTER_OTLP_*` env vars
128
+ when set, else `CODING_SH_URL` / `CODING_SH_INGEST_KEY`, and stamps `gen_ai.conversation.id` from
129
+ the current `with_session` / `trace(session_id=...)`.
130
+
131
+ ## Tests
132
+
133
+ ```sh
134
+ python3 -m unittest discover -s tests -t tests
135
+ ```
136
+
137
+ The provider tests use fake clients and always run. Tests against the real `openai`, `anthropic`
138
+ and OpenTelemetry packages run when those are installed.
@@ -0,0 +1,108 @@
1
+ # coding-sh (Python SDK)
2
+
3
+ Send LLM calls, traces and tool spans from any Python service to a
4
+ [coding.sh](https://coding.sh) server (`coding.sh serve`). Stdlib only, Python 3.9+.
5
+
6
+ ```sh
7
+ pip install coding-sh
8
+ export CODING_SH_URL=https://obs.example.com CODING_SH_INGEST_KEY=csh_ik_...
9
+ ```
10
+
11
+ ```python
12
+ import coding_sh
13
+ from openai import OpenAI
14
+
15
+ client = coding_sh.observe(OpenAI()) # also AsyncOpenAI, Anthropic, AsyncAnthropic
16
+ ```
17
+
18
+ That's it: every `chat.completions.create`, `responses.create` and `messages.create` /
19
+ `messages.stream` call (sync, async and streaming) becomes an `llm.call` with tokens, model,
20
+ latency, time to first token, stop reason and tool calls. The server prices it.
21
+
22
+ Without `CODING_SH_URL` the SDK does nothing. It never changes return values, errors or stream
23
+ semantics, and never raises into your app.
24
+
25
+ ## Traces, tools and conversations
26
+
27
+ ```python
28
+ with coding_sh.trace("support-reply", session_id=conversation_id, user_id=user.id, input=question) as t:
29
+ with coding_sh.span("tool", "lookup_order", input={"id": 42}) as s:
30
+ s.set_output(orders.get(42))
31
+ reply = client.chat.completions.create(model="gpt-4o-mini", messages=msgs)
32
+ t.set_output(reply.choices[0].message.content)
33
+ ```
34
+
35
+ - Each top-level `trace()` (or untraced call) is its own session. Pass `session_id` (or wrap code
36
+ in `coding_sh.with_session(id)`) to make each trace a turn of one conversation.
37
+ - `trace` and `span` are context managers (`with` / `async with`) and decorators. A decorated
38
+ `span("tool", ...)` records the call's arguments as input and its return value as output.
39
+ - `span("step", name)` records a generic step.
40
+ - Context follows `contextvars`, so it crosses `await` and asyncio tasks. Work handed to a thread
41
+ pool starts untraced unless you run it with `contextvars.copy_context().run`.
42
+
43
+ ## LLM gateways
44
+
45
+ Point the client at a gateway and each call records which one it went through, and the request id
46
+ the gateway's lookup API takes, so `coding.sh gateway enrich` can add the billed cost and
47
+ `coding.sh sessions --flag gateway:<name>` finds the sessions. Detected: Vercel AI Gateway,
48
+ OpenRouter, LiteLLM, Cloudflare AI Gateway, Portkey, Helicone, Requesty, Ramp Router (router.com),
49
+ TrueFoundry, Kong, Bifrost and Martian, by base URL (a self-hosted gateway by its URL env var, e.g.
50
+ `LITELLM_PROXY_API_BASE`), by response header (`cf-aig-log-id`, `helicone-id`, `x-litellm-call-id`,
51
+ `x-portkey-trace-id`, …) or by response id (`gen_…`, `gen-…`, `rqsty-…`).
52
+ `observe()` adds a response hook to the client's HTTP client that reads headers only, and only for
53
+ calls it records. [Details](https://github.com/jackulau/coding.sh/blob/main/docs/integrations/ai-gateway.md#from-the-sdks).
54
+
55
+ ```python
56
+ client = coding_sh.observe(OpenAI(base_url="https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/compat"))
57
+ ```
58
+
59
+ ## Delivery
60
+
61
+ One daemon thread sends batches (up to 500 events or 4 MiB, gzip) to `POST /v1/ingest`. It retries
62
+ 429, 5xx and network errors (honoring `Retry-After`, else 1, 2, 4 ... s, at most 5 attempts) with
63
+ stable event ids, so retries never duplicate. 400/401/403 drop the batch with one warning. A full
64
+ queue drops new events and counts them. An `atexit` flush is bounded to 2 s.
65
+
66
+ ```python
67
+ coding_sh.flush(timeout=5) # e.g. at the end of a script, a job or a serverless handler
68
+ coding_sh.shutdown() # flush, then stop
69
+ coding_sh.stats() # {"queued", "sent", "dropped", "failed"}
70
+ ```
71
+
72
+ ## Config
73
+
74
+ `coding_sh.init(url=, key=, service=, capture=, flush_interval_ms=1000, max_batch=500, max_queue=10000, disabled=, debug=)`
75
+ is optional; arguments override env vars.
76
+
77
+ | Env | Meaning |
78
+ |---|---|
79
+ | `CODING_SH_URL` | Server base URL. Unset = SDK off. |
80
+ | `CODING_SH_INGEST_KEY` | `csh_ik_...` ingest key. It decides the project. |
81
+ | `CODING_SH_SERVICE` (or `OTEL_SERVICE_NAME`) | Service name; becomes the session's agent id. |
82
+ | `CODING_SH_CAPTURE_MODE` | `full` (default) or `metadata` (no message text or tool I/O). |
83
+ | `CODING_SH_DISABLED=1` | Turn the SDK off. |
84
+ | `CODING_SH_DEBUG=1` | Log transport details (never keys or bodies). |
85
+
86
+ ## OpenTelemetry
87
+
88
+ Already emitting gen_ai spans? Point OTel at the server instead (no SDK needed), or let the
89
+ helper do the setup:
90
+
91
+ ```python
92
+ from coding_sh.otel import configure, otel_env
93
+ configure(service="checkout-api") # TracerProvider + OTLP/HTTP exporter + Bearer key
94
+ env = otel_env() # OTEL_EXPORTER_OTLP_* vars for a subprocess
95
+ ```
96
+
97
+ `pip install 'coding-sh[otel]'`. `configure()` uses the standard `OTEL_EXPORTER_OTLP_*` env vars
98
+ when set, else `CODING_SH_URL` / `CODING_SH_INGEST_KEY`, and stamps `gen_ai.conversation.id` from
99
+ the current `with_session` / `trace(session_id=...)`.
100
+
101
+ ## Tests
102
+
103
+ ```sh
104
+ python3 -m unittest discover -s tests -t tests
105
+ ```
106
+
107
+ The provider tests use fake clients and always run. Tests against the real `openai`, `anthropic`
108
+ and OpenTelemetry packages run when those are installed.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "coding-sh"
7
+ version = "0.1.3"
8
+ description = "Trace any AI agent or LLM app from Python: send model calls, traces and tool spans to a coding.sh server."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "coding.sh contributors" }]
13
+ requires-python = ">=3.9"
14
+ dependencies = []
15
+ keywords = ["ai agents", "observability", "evals", "opentelemetry", "llm", "claude code", "codex", "openai", "anthropic", "tracing"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: System :: Monitoring",
21
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ openai = ["openai>=1.0"]
27
+ anthropic = ["anthropic>=0.30"]
28
+ otel = ["opentelemetry-sdk>=1.20", "opentelemetry-exporter-otlp-proto-http>=1.20"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://coding.sh"
32
+ Repository = "https://github.com/jackulau/coding.sh"
33
+ Source = "https://github.com/jackulau/coding.sh/tree/main/packages/sdk-python"
34
+ Issues = "https://github.com/jackulau/coding.sh/issues"
35
+ Changelog = "https://github.com/jackulau/coding.sh/blob/main/CHANGELOG.md"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.setuptools.package-data]
41
+ coding_sh = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,33 @@
1
+ """coding.sh Python SDK: send LLM calls, traces and tool spans to a coding.sh server.
2
+
3
+ import coding_sh
4
+ from openai import OpenAI
5
+
6
+ client = coding_sh.observe(OpenAI()) # also AsyncOpenAI, Anthropic, AsyncAnthropic
7
+
8
+ with coding_sh.trace("support-reply", session_id=conversation_id, input=question):
9
+ with coding_sh.span("tool", "lookup_order", input={"id": 42}) as s:
10
+ s.set_output(lookup(42))
11
+ client.chat.completions.create(model="gpt-4o-mini", messages=[...])
12
+
13
+ Set CODING_SH_URL and CODING_SH_INGEST_KEY. Without a URL the SDK does nothing. It never
14
+ changes return values, errors or stream semantics, and never raises into the app.
15
+ Stdlib only; one daemon thread sends batches, and an atexit flush is bounded to 2 s.
16
+ """
17
+
18
+ from ._client import __version__
19
+ from ._providers import observe
20
+ from ._trace import current_session_id, flush, init, shutdown, span, stats, trace, with_session
21
+
22
+ __all__ = [
23
+ "__version__",
24
+ "init",
25
+ "observe",
26
+ "trace",
27
+ "span",
28
+ "with_session",
29
+ "current_session_id",
30
+ "flush",
31
+ "shutdown",
32
+ "stats",
33
+ ]