coding-sh 0.1.3__py3-none-any.whl

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.
coding_sh/otel.py ADDED
@@ -0,0 +1,144 @@
1
+ """OpenTelemetry gen_ai setup for coding.sh, for apps that already emit OTel spans.
2
+
3
+ coding.sh serve takes OTLP/HTTP at <url>/v1/traces, so plain OTel works with four env vars
4
+ (docs/PRODUCTION.md section 4.3). This module only saves the boilerplate:
5
+
6
+ from coding_sh.otel import configure
7
+ configure(service="checkout-api") # TracerProvider + OTLP/HTTP exporter + Bearer key
8
+
9
+ # or, for a subprocess / `opentelemetry-instrument`:
10
+ env = {**os.environ, **coding_sh.otel.otel_env()}
11
+
12
+ configure() resolves the endpoint like this: explicit `url`, then the standard
13
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT (left to the exporter, with
14
+ OTEL_EXPORTER_OTLP_HEADERS), then CODING_SH_URL + CODING_SH_INGEST_KEY. It also stamps
15
+ `gen_ai.conversation.id` on spans started inside coding_sh.with_session(...) or
16
+ coding_sh.trace(session_id=...), so OTel spans group into the same conversation session.
17
+
18
+ Needs `pip install coding-sh[otel]` (opentelemetry-sdk, opentelemetry-exporter-otlp-proto-http).
19
+ Without them, configure() warns once and returns None; it never raises into the app.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import os
26
+ import urllib.parse
27
+ from typing import Any, Dict, Optional
28
+
29
+ from ._trace import current_session_id
30
+
31
+ log = logging.getLogger("coding_sh")
32
+
33
+ CONVERSATION_ATTR = "gen_ai.conversation.id"
34
+
35
+
36
+ def _env(name: str) -> Optional[str]:
37
+ v = os.environ.get(name)
38
+ return v if v else None
39
+
40
+
41
+ def otel_env(url: Optional[str] = None, key: Optional[str] = None, service: Optional[str] = None) -> Dict[str, str]:
42
+ """OTEL_* exporter env vars that point any OTel SDK at a coding.sh server.
43
+
44
+ Defaults come from CODING_SH_URL, CODING_SH_INGEST_KEY and CODING_SH_SERVICE. The result
45
+ holds the ingest key: pass it to a child process, never log it.
46
+ """
47
+ url = url or _env("CODING_SH_URL")
48
+ key = key or _env("CODING_SH_INGEST_KEY")
49
+ service = service or _env("CODING_SH_SERVICE") or _env("OTEL_SERVICE_NAME")
50
+ env: Dict[str, str] = {}
51
+ if url:
52
+ env["OTEL_EXPORTER_OTLP_ENDPOINT"] = url.rstrip("/")
53
+ env["OTEL_EXPORTER_OTLP_PROTOCOL"] = "http/protobuf"
54
+ if key:
55
+ env["OTEL_EXPORTER_OTLP_HEADERS"] = "Authorization=" + urllib.parse.quote("Bearer " + key)
56
+ if service:
57
+ env["OTEL_SERVICE_NAME"] = service
58
+ return env
59
+
60
+
61
+ class ConversationSpanProcessor:
62
+ """Sets gen_ai.conversation.id from the current coding.sh session on every new span."""
63
+
64
+ def on_start(self, span: Any, parent_context: Any = None) -> None:
65
+ try:
66
+ sid = current_session_id()
67
+ if not sid:
68
+ return
69
+ attrs = getattr(span, "attributes", None) or {}
70
+ if CONVERSATION_ATTR not in attrs and "session.id" not in attrs:
71
+ span.set_attribute(CONVERSATION_ATTR, sid)
72
+ except Exception:
73
+ pass
74
+
75
+ def on_end(self, span: Any) -> None:
76
+ pass
77
+
78
+ def _on_ending(self, span: Any) -> None: # newer SDKs call this hook
79
+ pass
80
+
81
+ def shutdown(self) -> None:
82
+ pass
83
+
84
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
85
+ return True
86
+
87
+
88
+ _warned = False
89
+
90
+
91
+ def configure(
92
+ service: Optional[str] = None,
93
+ url: Optional[str] = None,
94
+ key: Optional[str] = None,
95
+ set_global: bool = True,
96
+ batch: bool = True,
97
+ ) -> Any:
98
+ """Build (and by default install) a TracerProvider that exports to coding.sh.
99
+
100
+ Returns the provider, or None when OpenTelemetry is not installed or no endpoint is known.
101
+ Call provider.shutdown() (or rely on the SDK's atexit hook) to flush on exit.
102
+ """
103
+ global _warned
104
+ try:
105
+ from opentelemetry import trace as otel_trace
106
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
107
+ from opentelemetry.sdk.resources import Resource
108
+ from opentelemetry.sdk.trace import TracerProvider
109
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
110
+ except Exception:
111
+ if not _warned:
112
+ _warned = True
113
+ log.warning("coding.sh: OpenTelemetry is not installed; pip install 'coding-sh[otel]'")
114
+ return None
115
+ try:
116
+ exporter_kwargs: Dict[str, Any] = {}
117
+ has_otel_endpoint = bool(_env("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or _env("OTEL_EXPORTER_OTLP_ENDPOINT"))
118
+ base = url or (None if has_otel_endpoint else _env("CODING_SH_URL"))
119
+ if base:
120
+ base = base.rstrip("/")
121
+ exporter_kwargs["endpoint"] = base if base.endswith("/v1/traces") else base + "/v1/traces"
122
+ elif not has_otel_endpoint:
123
+ if not _warned:
124
+ _warned = True
125
+ log.warning("coding.sh: no endpoint; set CODING_SH_URL or OTEL_EXPORTER_OTLP_ENDPOINT")
126
+ return None
127
+ k = key or (None if (has_otel_endpoint and not url) else _env("CODING_SH_INGEST_KEY"))
128
+ if k:
129
+ exporter_kwargs["headers"] = {"Authorization": "Bearer " + k}
130
+ name = service or _env("OTEL_SERVICE_NAME") or _env("CODING_SH_SERVICE")
131
+ resource = Resource.create({"service.name": name} if name else {})
132
+ provider = TracerProvider(resource=resource)
133
+ provider.add_span_processor(ConversationSpanProcessor()) # type: ignore[arg-type]
134
+ exporter = OTLPSpanExporter(**exporter_kwargs)
135
+ provider.add_span_processor(BatchSpanProcessor(exporter) if batch else SimpleSpanProcessor(exporter))
136
+ if set_global:
137
+ otel_trace.set_tracer_provider(provider)
138
+ return provider
139
+ except Exception as e:
140
+ log.warning("coding.sh: OpenTelemetry setup failed: %s", type(e).__name__)
141
+ return None
142
+
143
+
144
+ __all__ = ["configure", "otel_env", "ConversationSpanProcessor", "CONVERSATION_ATTR"]
coding_sh/py.typed ADDED
File without changes
@@ -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,14 @@
1
+ coding_sh/__init__.py,sha256=dtZ9V6DB_5J7MSuH3WE-1ZsW3RxZ9t-YWMeKgD3jNlM,1107
2
+ coding_sh/_client.py,sha256=xlyxT7ynsXs7SEdCOGwN0ljr1uvRJcSaYU3-emCO5YM,15877
3
+ coding_sh/_gateway.py,sha256=uzSNl9C-9n0q3M6V51owLScK0wQcB8kh2qrv2-JDjQ8,5401
4
+ coding_sh/_gateway_table.py,sha256=NeohPsRPgNhl7EBPy3u39eb5-mvbOPQ3JQbLfoMy-xk,3516
5
+ coding_sh/_llm.py,sha256=pOwT5YTA7dyJ_dAGptICy8vQVlhPvF9hp76pqMXEDwQ,11003
6
+ coding_sh/_providers.py,sha256=Kz2tXqctbysTt3eZpd5ws1wXE1DoIhE74gCodeKlB_g,19195
7
+ coding_sh/_trace.py,sha256=OdSOn_IiX_EsaItF1W_MKRVaE3-qy2d80f6icqeILBs,21979
8
+ coding_sh/otel.py,sha256=_eq1E6ae5Yk-OGwja9f187y2BkUeFg_uV8R5Vp5SrJk,5773
9
+ coding_sh/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ coding_sh-0.1.3.dist-info/licenses/LICENSE,sha256=uDSs7rzgBZqwq0KlOzhLZnBPeBfTcGI8ijcPbxQ2tHc,1079
11
+ coding_sh-0.1.3.dist-info/METADATA,sha256=ZFS0kS9YNrILVv8X_mgOhRqQAlU3sVM4NMYmBBj8erk,6318
12
+ coding_sh-0.1.3.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ coding_sh-0.1.3.dist-info/top_level.txt,sha256=rbbP9kA9i8EteSHOy7U6-bB9AQetzeu17i-Gluy684U,10
14
+ coding_sh-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ coding_sh