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.
@@ -0,0 +1,117 @@
1
+ # Generated from packages/coding.sh/src/integrations/gateway/providers.ts. Do not edit:
2
+ # change the registry, then run `node packages/coding.sh/scripts/sdk-gateways.ts`.
3
+ #
4
+ # One dict per gateway: id, hosts (exact, or a parent domain when it starts with "."), url_env
5
+ # (env vars naming a self-hosted gateway's URL), id_pattern (request id shape), id_headers
6
+ # (response headers carrying the request id `gateway enrich` looks up), mark_headers (other
7
+ # response headers only this gateway sets).
8
+
9
+ import re
10
+ from typing import Any, Dict, Tuple
11
+
12
+ GATEWAY_TABLE: Tuple[Dict[str, Any], ...] = (
13
+ {
14
+ "id": "vercel",
15
+ "hosts": ("ai-gateway.vercel.sh",),
16
+ "url_env": (),
17
+ "id_pattern": re.compile("^gen_[0-9A-HJKMNP-TV-Z]{26}$", re.IGNORECASE),
18
+ "id_headers": ("x-generation-id",),
19
+ "mark_headers": (),
20
+ },
21
+ {
22
+ "id": "openrouter",
23
+ "hosts": ("openrouter.ai",),
24
+ "url_env": (),
25
+ "id_pattern": re.compile("^gen-[0-9A-Za-z][0-9A-Za-z-]{11,}$"),
26
+ "id_headers": ("x-generation-id",),
27
+ "mark_headers": (),
28
+ },
29
+ {
30
+ "id": "litellm",
31
+ "hosts": (),
32
+ "url_env": ("LITELLM_PROXY_API_BASE", "LITELLM_PROXY_URL", "LITELLM_BASE_URL"),
33
+ "id_pattern": None,
34
+ "id_headers": ("x-litellm-call-id",),
35
+ "mark_headers": ("x-litellm-response-cost", "x-litellm-model-api-base"),
36
+ },
37
+ {
38
+ "id": "cloudflare",
39
+ "hosts": ("gateway.ai.cloudflare.com",),
40
+ "url_env": (),
41
+ "id_pattern": None,
42
+ "id_headers": ("cf-aig-log-id",),
43
+ "mark_headers": (),
44
+ },
45
+ {
46
+ "id": "portkey",
47
+ "hosts": ("api.portkey.ai",),
48
+ "url_env": ("PORTKEY_GATEWAY_URL",),
49
+ "id_pattern": None,
50
+ "id_headers": ("x-portkey-trace-id",),
51
+ "mark_headers": (),
52
+ },
53
+ {
54
+ "id": "helicone",
55
+ "hosts": (".helicone.ai",),
56
+ "url_env": (),
57
+ "id_pattern": None,
58
+ "id_headers": ("helicone-id",),
59
+ "mark_headers": (),
60
+ },
61
+ {
62
+ "id": "requesty",
63
+ "hosts": ("router.requesty.ai", "router.eu.requesty.ai"),
64
+ "url_env": (),
65
+ "id_pattern": re.compile("^rqsty-[A-Za-z0-9-]{8,}$"),
66
+ "id_headers": (),
67
+ "mark_headers": (),
68
+ },
69
+ {
70
+ "id": "ramp",
71
+ "hosts": ("api.router.com",),
72
+ "url_env": ("RAMP_ROUTER_BASE_URL",),
73
+ "id_pattern": None,
74
+ "id_headers": (),
75
+ "mark_headers": ("x-router-provider",),
76
+ },
77
+ {
78
+ "id": "truefoundry",
79
+ "hosts": (".truefoundry.ai", ".truefoundry.com", ".truefoundry.cloud"),
80
+ "url_env": ("TFY_GATEWAY_URL",),
81
+ "id_pattern": None,
82
+ "id_headers": (),
83
+ "mark_headers": (),
84
+ },
85
+ {
86
+ "id": "kong",
87
+ "hosts": (),
88
+ "url_env": ("KONG_PROXY_URL",),
89
+ "id_pattern": None,
90
+ "id_headers": ("x-kong-request-id",),
91
+ "mark_headers": ("x-kong-llm-model",),
92
+ },
93
+ {
94
+ "id": "bifrost",
95
+ "hosts": (),
96
+ "url_env": ("BIFROST_URL", "BIFROST_BASE_URL"),
97
+ "id_pattern": None,
98
+ "id_headers": (),
99
+ "mark_headers": (),
100
+ },
101
+ {
102
+ "id": "martian",
103
+ "hosts": ("withmartian.com",),
104
+ "url_env": (),
105
+ "id_pattern": None,
106
+ "id_headers": (),
107
+ "mark_headers": (),
108
+ },
109
+ {
110
+ "id": "cliproxyapi",
111
+ "hosts": (),
112
+ "url_env": ("CLIPROXYAPI_URL",),
113
+ "id_pattern": None,
114
+ "id_headers": (),
115
+ "mark_headers": (),
116
+ },
117
+ )
coding_sh/_llm.py ADDED
@@ -0,0 +1,323 @@
1
+ """Model-call recording and stream proxies shared by the provider wrappers.
2
+
3
+ The wrappers never change what the app sees: return values are the provider's own objects
4
+ (streams are proxied, every other attribute passes through), exceptions propagate unchanged,
5
+ and recording errors are swallowed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+ from ._client import dumps
14
+ from ._gateway import detect_gateway
15
+ from ._trace import Scope, clip, emit, full_capture
16
+
17
+
18
+ def g(obj: Any, key: str, default: Any = None) -> Any:
19
+ """Read a field from a pydantic model, a plain object or a dict."""
20
+ if obj is None:
21
+ return default
22
+ if isinstance(obj, dict):
23
+ return obj.get(key, default)
24
+ try:
25
+ return getattr(obj, key, default)
26
+ except Exception:
27
+ return default
28
+
29
+
30
+ def given(v: Any) -> bool:
31
+ """False for None and the SDKs' NOT_GIVEN / omit sentinels."""
32
+ return v is not None and type(v).__name__ not in ("NotGiven", "Omit")
33
+
34
+
35
+ def _num(v: Any) -> Optional[float]:
36
+ if isinstance(v, bool) or not isinstance(v, (int, float)):
37
+ return None
38
+ return v if v >= 0 else None
39
+
40
+
41
+ def content_text(content: Any) -> Optional[str]:
42
+ """Text of a message `content`: a string, or a list of text parts/blocks."""
43
+ if isinstance(content, str):
44
+ return content
45
+ if isinstance(content, (list, tuple)):
46
+ parts = []
47
+ for p in content:
48
+ if isinstance(p, str):
49
+ parts.append(p)
50
+ continue
51
+ t = g(p, "type")
52
+ if t in (None, "text", "input_text", "output_text"):
53
+ txt = g(p, "text")
54
+ if isinstance(txt, str) and txt:
55
+ parts.append(txt)
56
+ return "\n".join(parts) if parts else None
57
+ return None
58
+
59
+
60
+ def last_user_text(messages: Any) -> Optional[str]:
61
+ if isinstance(messages, str):
62
+ return messages
63
+ if not isinstance(messages, (list, tuple)):
64
+ return None
65
+ for m in reversed(messages):
66
+ if g(m, "role") == "user":
67
+ t = content_text(g(m, "content"))
68
+ if t:
69
+ return t
70
+ return None
71
+
72
+
73
+ class LLMCall(Scope):
74
+ """One model API call: llm.call + assistant.message (plus an implicit root when untraced)."""
75
+
76
+ bind_context = False
77
+
78
+ def __init__(self, provider: str, request_model: Any, input_fn: Callable[[], Optional[str]]) -> None:
79
+ super().__init__()
80
+ self.provider = provider
81
+ self.operation = "chat"
82
+ self.request_model = request_model if isinstance(request_model, str) else None
83
+ self._input_fn = input_fn
84
+ self.response_model: Optional[str] = None
85
+ self.text_parts: List[str] = []
86
+ self.stop_reason: Optional[str] = None
87
+ self.usage: Optional[Dict[str, float]] = None
88
+ self.tool_calls: Dict[Any, Dict[str, Any]] = {}
89
+ self.ttft_ms: Optional[float] = None
90
+ self.response_id: Optional[str] = None
91
+ # Which gateway, if any: the client's base URL, and the response's URL and headers.
92
+ self.base_url: Optional[str] = None
93
+ self.request_url: Optional[str] = None
94
+ self.response_headers: Any = None
95
+
96
+ def mark_first_token(self) -> None:
97
+ if self.ttft_ms is None and self._p0:
98
+ self.ttft_ms = round((time.perf_counter() - self._p0) * 1000.0, 3)
99
+
100
+ def add_tool_call(self, key: Any, id: Any = None, name: Any = None, arguments: Any = None, args_delta: Any = None) -> None:
101
+ tc = self.tool_calls.setdefault(key, {})
102
+ if isinstance(id, str) and id:
103
+ tc["id"] = id
104
+ if isinstance(name, str) and name:
105
+ tc["name"] = name
106
+ if arguments is not None:
107
+ tc["arguments"] = arguments
108
+ if isinstance(args_delta, str) and args_delta:
109
+ tc["arguments"] = (tc.get("arguments") if isinstance(tc.get("arguments"), str) else "") + args_delta
110
+
111
+ def set_response_id(self, v: Any) -> None:
112
+ if isinstance(v, str) and v:
113
+ self.response_id = v
114
+
115
+ def _tool_calls_payload(self) -> List[Dict[str, Any]]:
116
+ out = []
117
+ for tc in self.tool_calls.values():
118
+ if not tc.get("name"):
119
+ continue
120
+ o: Dict[str, Any] = {"name": tc["name"]}
121
+ if tc.get("id"):
122
+ o["id"] = tc["id"]
123
+ args = tc.get("arguments")
124
+ if args is not None and full_capture():
125
+ o["arguments"] = clip(args if isinstance(args, str) else dumps(args))
126
+ out.append(o)
127
+ return out
128
+
129
+ def _root_name(self) -> str:
130
+ return self.operation + (" " + self.request_model if self.request_model else "")
131
+
132
+ def _root_input(self) -> Optional[str]:
133
+ try:
134
+ return self._input_fn()
135
+ except Exception:
136
+ return None
137
+
138
+ def _on_end(self, error: Optional[BaseException], duration: float, end_ts: float) -> None:
139
+ if self.node is None:
140
+ return
141
+ from ._trace import error_text
142
+
143
+ gw = None
144
+ try:
145
+ gw = detect_gateway(self.request_url or self.base_url, self.response_headers, self.response_id)
146
+ except Exception:
147
+ gw = None
148
+ p: Dict[str, Any] = {
149
+ "provider": self.provider,
150
+ "requestModel": self.request_model,
151
+ "responseModel": self.response_model,
152
+ "operation": self.operation,
153
+ "ttftMs": self.ttft_ms,
154
+ "stopReason": self.stop_reason,
155
+ "ok": error is None,
156
+ "error": error_text(error) if error else None,
157
+ "responseId": self.response_id,
158
+ # The names the server reads: gateway -> the `gateway:<name>` flag, gatewayGenerationId -> `gateway enrich`.
159
+ "gateway": gw[0] if gw else None,
160
+ "gatewayGenerationId": gw[1] if gw else None,
161
+ }
162
+ calls = self._tool_calls_payload()
163
+ if calls:
164
+ p["toolCalls"] = calls
165
+ out = (self.usage or {}).get("outputTokens")
166
+ if out and duration > 0:
167
+ p["tokensPerSec"] = round(out / (duration / 1000.0), 2)
168
+ model = self.response_model or self.request_model
169
+ nested = not self.is_root
170
+ emit(self.node.event("llm.call", self._t0, p, anchor=nested, child=nested, durationMs=duration, model=model, usage=self.usage or None))
171
+ text = "".join(self.text_parts)
172
+ if text and full_capture():
173
+ emit(self.node.event("assistant.message", end_ts, {"text": clip(text)}, child=nested, model=model))
174
+
175
+
176
+ # ---------------------------------------------------------------------------- streams
177
+
178
+
179
+ class SyncStream:
180
+ """Proxy over a provider's sync stream: same items, same attributes, records on the side."""
181
+
182
+ def __init__(self, stream: Any, call: LLMCall, observe: Callable[[LLMCall, Any], bool]) -> None:
183
+ self._cs_stream = stream
184
+ self._cs_call = call
185
+ self._cs_observe = observe
186
+ self._cs_it: Any = None
187
+
188
+ def _cs_finish(self, error: Optional[BaseException] = None) -> None:
189
+ try:
190
+ self._cs_call.end(error)
191
+ except Exception:
192
+ pass
193
+
194
+ def __iter__(self) -> "SyncStream":
195
+ return self
196
+
197
+ def __next__(self) -> Any:
198
+ if self._cs_it is None:
199
+ self._cs_it = iter(self._cs_stream)
200
+ while True:
201
+ try:
202
+ item = next(self._cs_it)
203
+ except StopIteration:
204
+ self._cs_finish()
205
+ raise
206
+ except Exception as e:
207
+ self._cs_finish(e)
208
+ raise
209
+ except BaseException:
210
+ self._cs_finish()
211
+ raise
212
+ swallow = False
213
+ try:
214
+ swallow = self._cs_observe(self._cs_call, item)
215
+ except Exception:
216
+ pass
217
+ if not swallow:
218
+ return item
219
+
220
+ def __enter__(self) -> "SyncStream":
221
+ enter = getattr(self._cs_stream, "__enter__", None)
222
+ if enter is not None:
223
+ enter()
224
+ return self
225
+
226
+ def __exit__(self, *exc: Any) -> Any:
227
+ try:
228
+ ex = getattr(self._cs_stream, "__exit__", None)
229
+ return ex(*exc) if ex is not None else None
230
+ finally:
231
+ self._cs_finish(exc[1] if len(exc) > 1 and isinstance(exc[1], Exception) else None)
232
+
233
+ def close(self) -> Any:
234
+ try:
235
+ return self._cs_stream.close()
236
+ finally:
237
+ self._cs_finish()
238
+
239
+ def __getattr__(self, name: str) -> Any:
240
+ if name.startswith("_cs_"):
241
+ raise AttributeError(name)
242
+ return getattr(self._cs_stream, name)
243
+
244
+ def __del__(self) -> None:
245
+ try:
246
+ self._cs_finish()
247
+ except Exception:
248
+ pass
249
+
250
+
251
+ class AsyncStream:
252
+ """Proxy over a provider's async stream."""
253
+
254
+ def __init__(self, stream: Any, call: LLMCall, observe: Callable[[LLMCall, Any], bool]) -> None:
255
+ self._cs_stream = stream
256
+ self._cs_call = call
257
+ self._cs_observe = observe
258
+ self._cs_it: Any = None
259
+
260
+ def _cs_finish(self, error: Optional[BaseException] = None) -> None:
261
+ try:
262
+ self._cs_call.end(error)
263
+ except Exception:
264
+ pass
265
+
266
+ def __aiter__(self) -> "AsyncStream":
267
+ return self
268
+
269
+ async def __anext__(self) -> Any:
270
+ if self._cs_it is None:
271
+ self._cs_it = self._cs_stream.__aiter__()
272
+ while True:
273
+ try:
274
+ item = await self._cs_it.__anext__()
275
+ except StopAsyncIteration:
276
+ self._cs_finish()
277
+ raise
278
+ except Exception as e:
279
+ self._cs_finish(e)
280
+ raise
281
+ except BaseException:
282
+ self._cs_finish()
283
+ raise
284
+ swallow = False
285
+ try:
286
+ swallow = self._cs_observe(self._cs_call, item)
287
+ except Exception:
288
+ pass
289
+ if not swallow:
290
+ return item
291
+
292
+ async def __aenter__(self) -> "AsyncStream":
293
+ enter = getattr(self._cs_stream, "__aenter__", None)
294
+ if enter is not None:
295
+ await enter()
296
+ return self
297
+
298
+ async def __aexit__(self, *exc: Any) -> Any:
299
+ try:
300
+ ex = getattr(self._cs_stream, "__aexit__", None)
301
+ return (await ex(*exc)) if ex is not None else None
302
+ finally:
303
+ self._cs_finish(exc[1] if len(exc) > 1 and isinstance(exc[1], Exception) else None)
304
+
305
+ async def close(self) -> Any:
306
+ try:
307
+ r = self._cs_stream.close()
308
+ if hasattr(r, "__await__"):
309
+ r = await r
310
+ return r
311
+ finally:
312
+ self._cs_finish()
313
+
314
+ def __getattr__(self, name: str) -> Any:
315
+ if name.startswith("_cs_"):
316
+ raise AttributeError(name)
317
+ return getattr(self._cs_stream, name)
318
+
319
+ def __del__(self) -> None:
320
+ try:
321
+ self._cs_finish()
322
+ except Exception:
323
+ pass