tensorcost 0.4.0__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.
- tensorcost/__init__.py +61 -0
- tensorcost/_applied_mode.py +148 -0
- tensorcost/_circuit.py +99 -0
- tensorcost/_config.py +156 -0
- tensorcost/_errors.py +122 -0
- tensorcost/_models.py +40 -0
- tensorcost/_providers/__init__.py +1 -0
- tensorcost/_providers/_anthropic.py +266 -0
- tensorcost/_providers/_detect.py +46 -0
- tensorcost/_providers/_openai.py +295 -0
- tensorcost/_proxy_client.py +408 -0
- tensorcost/_retry.py +99 -0
- tensorcost/_telemetry.py +120 -0
- tensorcost/_transport.py +174 -0
- tensorcost/_wrap.py +162 -0
- tensorcost/py.typed +0 -0
- tensorcost-0.4.0.dist-info/METADATA +281 -0
- tensorcost-0.4.0.dist-info/RECORD +20 -0
- tensorcost-0.4.0.dist-info/WHEEL +4 -0
- tensorcost-0.4.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Anthropic client wrapping.
|
|
2
|
+
|
|
3
|
+
Intercepts ``client.messages.create``. Same shape and guarantees as the
|
|
4
|
+
OpenAI wrapper: metadata-only capture, fail-open on observation errors.
|
|
5
|
+
|
|
6
|
+
Applied-mode hardening (v0.4.0): messages.create calls route through the
|
|
7
|
+
proxy-client module, which handles retries, timeouts, typed errors,
|
|
8
|
+
telemetry hooks, and the fail-open circuit breaker.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import logging
|
|
15
|
+
import uuid
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from .._applied_mode import (
|
|
20
|
+
anthropic_provider_auth,
|
|
21
|
+
anthropic_provider_base_url,
|
|
22
|
+
)
|
|
23
|
+
from .._circuit import CircuitBreaker
|
|
24
|
+
from .._errors import TensorCostProxyError
|
|
25
|
+
from .._models import Observation
|
|
26
|
+
from .._proxy_client import proxy_request
|
|
27
|
+
from .._retry import DEFAULT_RETRY_CONFIG, RetryConfig
|
|
28
|
+
from .._telemetry import LifecycleEventCallback, OnFallbackEvent, emit_event
|
|
29
|
+
from .._transport import ObservationTransport
|
|
30
|
+
|
|
31
|
+
_SDK_VERSION = "tensorcost-python/0.4.0"
|
|
32
|
+
_log = logging.getLogger("tensorcost")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now_iso() -> str:
|
|
36
|
+
return datetime.now(timezone.utc).isoformat()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _extract_usage(response: Any) -> tuple[int | None, int | None]:
|
|
40
|
+
usage = getattr(response, "usage", None)
|
|
41
|
+
if usage is None and isinstance(response, dict):
|
|
42
|
+
usage = response.get("usage")
|
|
43
|
+
if usage is None:
|
|
44
|
+
return None, None
|
|
45
|
+
if isinstance(usage, dict):
|
|
46
|
+
return usage.get("input_tokens"), usage.get("output_tokens")
|
|
47
|
+
return (
|
|
48
|
+
getattr(usage, "input_tokens", None),
|
|
49
|
+
getattr(usage, "output_tokens", None),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _post_obs(
|
|
54
|
+
transport: ObservationTransport,
|
|
55
|
+
model: str,
|
|
56
|
+
tenant_id: str | None,
|
|
57
|
+
environment: str | None,
|
|
58
|
+
connection_id: str | None,
|
|
59
|
+
correlation_id: str,
|
|
60
|
+
request_at: str,
|
|
61
|
+
input_tokens: int | None,
|
|
62
|
+
output_tokens: int | None,
|
|
63
|
+
status: str,
|
|
64
|
+
error_message: str | None,
|
|
65
|
+
) -> None:
|
|
66
|
+
obs: dict = {
|
|
67
|
+
"sdk_version": _SDK_VERSION,
|
|
68
|
+
"provider": "anthropic",
|
|
69
|
+
"model": model,
|
|
70
|
+
"operation": "messages",
|
|
71
|
+
"request_at": request_at,
|
|
72
|
+
"response_at": _now_iso(),
|
|
73
|
+
"input_tokens": input_tokens,
|
|
74
|
+
"output_tokens": output_tokens,
|
|
75
|
+
"cost_usd_cents": None,
|
|
76
|
+
"status": status,
|
|
77
|
+
"error_message": error_message,
|
|
78
|
+
"correlation_id": correlation_id,
|
|
79
|
+
"tenant_id": tenant_id,
|
|
80
|
+
**({"environment": environment} if environment is not None else {}),
|
|
81
|
+
**({"connection_id": connection_id} if connection_id is not None else {}),
|
|
82
|
+
}
|
|
83
|
+
try:
|
|
84
|
+
transport.post(obs)
|
|
85
|
+
except Exception: # noqa: BLE001
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _wrap_method(
|
|
90
|
+
bound_method: Any,
|
|
91
|
+
*,
|
|
92
|
+
transport: ObservationTransport,
|
|
93
|
+
tenant_id: str | None,
|
|
94
|
+
environment: str | None,
|
|
95
|
+
connection_id: str | None,
|
|
96
|
+
applied_mode: bool = False,
|
|
97
|
+
proxy_url: Optional[str] = None,
|
|
98
|
+
client_ref: Optional[Any] = None,
|
|
99
|
+
retry: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
100
|
+
timeout_s: float = 60.0,
|
|
101
|
+
on_lifecycle_event: Optional[LifecycleEventCallback] = None,
|
|
102
|
+
fail_open_enabled: bool = True,
|
|
103
|
+
circuit: Optional[CircuitBreaker] = None,
|
|
104
|
+
) -> Any:
|
|
105
|
+
@functools.wraps(bound_method)
|
|
106
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
107
|
+
request_at = _now_iso()
|
|
108
|
+
model = kwargs.get("model") or (args[0] if args else "unknown")
|
|
109
|
+
correlation_id = str(uuid.uuid4())
|
|
110
|
+
|
|
111
|
+
# Applied-mode path.
|
|
112
|
+
if applied_mode and proxy_url:
|
|
113
|
+
provider_url = anthropic_provider_base_url(client_ref) if client_ref else "https://api.anthropic.com"
|
|
114
|
+
provider_auth = anthropic_provider_auth(client_ref) if client_ref else ""
|
|
115
|
+
|
|
116
|
+
body: dict[str, Any] = dict(kwargs)
|
|
117
|
+
body["model"] = str(model)
|
|
118
|
+
|
|
119
|
+
circuit_was_open = circuit is not None and circuit.should_bypass()
|
|
120
|
+
proxy_failed = False
|
|
121
|
+
proxy_error: Optional[Exception] = None
|
|
122
|
+
proxy_result: Optional[Any] = None
|
|
123
|
+
|
|
124
|
+
if not circuit_was_open:
|
|
125
|
+
try:
|
|
126
|
+
bearer_token = transport._get_token()
|
|
127
|
+
proxy_result = proxy_request(
|
|
128
|
+
proxy_url=proxy_url,
|
|
129
|
+
bearer_token=bearer_token,
|
|
130
|
+
provider_url=provider_url,
|
|
131
|
+
provider_auth=provider_auth,
|
|
132
|
+
body=body,
|
|
133
|
+
model=str(model),
|
|
134
|
+
provider="anthropic",
|
|
135
|
+
operation="messages",
|
|
136
|
+
correlation_id=correlation_id,
|
|
137
|
+
environment=environment,
|
|
138
|
+
retry=retry,
|
|
139
|
+
timeout_s=timeout_s,
|
|
140
|
+
on_lifecycle_event=on_lifecycle_event,
|
|
141
|
+
circuit=circuit,
|
|
142
|
+
)
|
|
143
|
+
except Exception as exc: # noqa: BLE001
|
|
144
|
+
proxy_failed = True
|
|
145
|
+
proxy_error = exc
|
|
146
|
+
else:
|
|
147
|
+
proxy_failed = True
|
|
148
|
+
|
|
149
|
+
if proxy_failed or circuit_was_open:
|
|
150
|
+
if not fail_open_enabled:
|
|
151
|
+
tc_err = (
|
|
152
|
+
proxy_error
|
|
153
|
+
if isinstance(proxy_error, TensorCostProxyError)
|
|
154
|
+
else TensorCostProxyError(
|
|
155
|
+
str(proxy_error) if proxy_error else "proxy unavailable",
|
|
156
|
+
root_cause=proxy_error if isinstance(proxy_error, Exception) else None,
|
|
157
|
+
attempt=1,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
_post_obs(
|
|
161
|
+
transport, str(model), tenant_id, environment,
|
|
162
|
+
connection_id, correlation_id, request_at, None, None,
|
|
163
|
+
"error", f"{type(tc_err).__name__}: {tc_err}",
|
|
164
|
+
)
|
|
165
|
+
raise tc_err
|
|
166
|
+
|
|
167
|
+
emit_event(
|
|
168
|
+
on_lifecycle_event,
|
|
169
|
+
OnFallbackEvent(
|
|
170
|
+
kind="on_fallback",
|
|
171
|
+
provider="anthropic",
|
|
172
|
+
model=str(model),
|
|
173
|
+
operation="messages",
|
|
174
|
+
attempt_number=1,
|
|
175
|
+
elapsed_ms=0.0,
|
|
176
|
+
consecutive_failures=circuit.failures if circuit else 0,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
try:
|
|
180
|
+
direct_response = bound_method(*args, **kwargs)
|
|
181
|
+
input_tokens, output_tokens = _extract_usage(direct_response)
|
|
182
|
+
_post_obs(
|
|
183
|
+
transport, str(model), tenant_id, environment,
|
|
184
|
+
connection_id, correlation_id, request_at, input_tokens,
|
|
185
|
+
output_tokens, "success", None,
|
|
186
|
+
)
|
|
187
|
+
return direct_response
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
_post_obs(
|
|
190
|
+
transport, str(model), tenant_id, environment,
|
|
191
|
+
connection_id, correlation_id, request_at, None, None,
|
|
192
|
+
"error", f"{type(exc).__name__}: {exc}",
|
|
193
|
+
)
|
|
194
|
+
raise
|
|
195
|
+
|
|
196
|
+
# Proxy success.
|
|
197
|
+
resp_dict = proxy_result if isinstance(proxy_result, dict) else {}
|
|
198
|
+
usage = resp_dict.get("usage") if isinstance(resp_dict, dict) else None
|
|
199
|
+
input_tokens = usage.get("input_tokens") if isinstance(usage, dict) else None
|
|
200
|
+
output_tokens = usage.get("output_tokens") if isinstance(usage, dict) else None
|
|
201
|
+
_post_obs(
|
|
202
|
+
transport, str(model), tenant_id, environment,
|
|
203
|
+
connection_id, correlation_id, request_at, input_tokens,
|
|
204
|
+
output_tokens, "success", None,
|
|
205
|
+
)
|
|
206
|
+
return proxy_result
|
|
207
|
+
|
|
208
|
+
# Observe-only path.
|
|
209
|
+
try:
|
|
210
|
+
response = bound_method(*args, **kwargs)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
_post_obs(
|
|
213
|
+
transport, str(model), tenant_id, environment,
|
|
214
|
+
connection_id, correlation_id, request_at, None, None,
|
|
215
|
+
"error", f"{type(exc).__name__}: {exc}",
|
|
216
|
+
)
|
|
217
|
+
raise
|
|
218
|
+
|
|
219
|
+
input_tokens, output_tokens = _extract_usage(response)
|
|
220
|
+
_post_obs(
|
|
221
|
+
transport, str(model), tenant_id, environment,
|
|
222
|
+
connection_id, correlation_id, request_at, input_tokens,
|
|
223
|
+
output_tokens, "success", None,
|
|
224
|
+
)
|
|
225
|
+
return response
|
|
226
|
+
|
|
227
|
+
setattr(wrapper, "__tensorcost_wrapped__", True)
|
|
228
|
+
return wrapper
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def install(
|
|
232
|
+
client: Any,
|
|
233
|
+
transport: ObservationTransport,
|
|
234
|
+
tenant_id: str | None,
|
|
235
|
+
environment: str | None,
|
|
236
|
+
connection_id: str | None,
|
|
237
|
+
applied_mode: bool = False,
|
|
238
|
+
proxy_url: Optional[str] = None,
|
|
239
|
+
retry: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
240
|
+
timeout_s: float = 60.0,
|
|
241
|
+
on_lifecycle_event: Optional[LifecycleEventCallback] = None,
|
|
242
|
+
fail_open_enabled: bool = True,
|
|
243
|
+
circuit: Optional[CircuitBreaker] = None,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Monkey-patch ``client.messages.create`` in place."""
|
|
246
|
+
messages = getattr(client, "messages", None)
|
|
247
|
+
if messages is None or not callable(getattr(messages, "create", None)):
|
|
248
|
+
return
|
|
249
|
+
original = messages.create
|
|
250
|
+
if getattr(original, "__tensorcost_wrapped__", False):
|
|
251
|
+
return
|
|
252
|
+
messages.create = _wrap_method(
|
|
253
|
+
original,
|
|
254
|
+
transport=transport,
|
|
255
|
+
tenant_id=tenant_id,
|
|
256
|
+
environment=environment,
|
|
257
|
+
connection_id=connection_id,
|
|
258
|
+
applied_mode=applied_mode,
|
|
259
|
+
proxy_url=proxy_url,
|
|
260
|
+
client_ref=client,
|
|
261
|
+
retry=retry,
|
|
262
|
+
timeout_s=timeout_s,
|
|
263
|
+
on_lifecycle_event=on_lifecycle_event,
|
|
264
|
+
fail_open_enabled=fail_open_enabled,
|
|
265
|
+
circuit=circuit,
|
|
266
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Provider detection via duck-typing.
|
|
2
|
+
|
|
3
|
+
We deliberately avoid ``isinstance`` checks against the OpenAI /
|
|
4
|
+
Anthropic SDK classes so that the SDK has zero hard imports of those
|
|
5
|
+
packages. A customer who uses only OpenAI shouldn't need ``anthropic``
|
|
6
|
+
installed (and vice versa).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UnsupportedClientError(TypeError):
|
|
15
|
+
"""Raised by :func:`detect_provider` when the client shape is not
|
|
16
|
+
recognized as either an OpenAI or Anthropic client."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _looks_like_openai(client: Any) -> bool:
|
|
20
|
+
chat = getattr(client, "chat", None)
|
|
21
|
+
completions = getattr(chat, "completions", None) if chat is not None else None
|
|
22
|
+
return callable(getattr(completions, "create", None))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _looks_like_anthropic(client: Any) -> bool:
|
|
26
|
+
# Anthropic clients expose ``client.messages.create``; we additionally
|
|
27
|
+
# require the *absence* of ``client.chat`` to disambiguate from OpenAI.
|
|
28
|
+
if getattr(client, "chat", None) is not None:
|
|
29
|
+
return False
|
|
30
|
+
messages = getattr(client, "messages", None)
|
|
31
|
+
return callable(getattr(messages, "create", None))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def detect_provider(client: Any) -> str:
|
|
35
|
+
"""Return ``"openai"`` or ``"anthropic"`` based on duck-typing.
|
|
36
|
+
|
|
37
|
+
Raises :class:`UnsupportedClientError` if neither shape matches.
|
|
38
|
+
"""
|
|
39
|
+
if _looks_like_openai(client):
|
|
40
|
+
return "openai"
|
|
41
|
+
if _looks_like_anthropic(client):
|
|
42
|
+
return "anthropic"
|
|
43
|
+
raise UnsupportedClientError(
|
|
44
|
+
"tensorcost.wrap(): client does not look like an OpenAI or Anthropic "
|
|
45
|
+
"client. Supported in v0.1.0: openai>=1.0, anthropic>=0.20."
|
|
46
|
+
)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""OpenAI client wrapping.
|
|
2
|
+
|
|
3
|
+
Intercepts:
|
|
4
|
+
* ``client.chat.completions.create``
|
|
5
|
+
* ``client.completions.create`` (legacy text completions; v0 still ships)
|
|
6
|
+
|
|
7
|
+
We capture only metadata (model, message count, token usage) — never
|
|
8
|
+
prompt content. Customers' prompts stay on their machine.
|
|
9
|
+
|
|
10
|
+
Applied-mode hardening (v0.4.0): chat/completions calls route through the
|
|
11
|
+
proxy-client module, which handles retries, timeouts, typed errors,
|
|
12
|
+
telemetry hooks, and the fail-open circuit breaker.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import logging
|
|
19
|
+
import uuid
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
from .._applied_mode import (
|
|
24
|
+
openai_provider_auth,
|
|
25
|
+
openai_provider_base_url,
|
|
26
|
+
)
|
|
27
|
+
from .._circuit import CircuitBreaker
|
|
28
|
+
from .._errors import TensorCostProxyError
|
|
29
|
+
from .._models import Observation
|
|
30
|
+
from .._proxy_client import proxy_request
|
|
31
|
+
from .._retry import DEFAULT_RETRY_CONFIG, RetryConfig
|
|
32
|
+
from .._telemetry import LifecycleEventCallback, OnFallbackEvent, emit_event
|
|
33
|
+
from .._transport import ObservationTransport
|
|
34
|
+
|
|
35
|
+
_SDK_VERSION = "tensorcost-python/0.4.0"
|
|
36
|
+
_log = logging.getLogger("tensorcost")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _now_iso() -> str:
|
|
40
|
+
return datetime.now(timezone.utc).isoformat()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _extract_usage(response: Any) -> tuple[int | None, int | None]:
|
|
44
|
+
usage = getattr(response, "usage", None)
|
|
45
|
+
if usage is None and isinstance(response, dict):
|
|
46
|
+
usage = response.get("usage")
|
|
47
|
+
if usage is None:
|
|
48
|
+
return None, None
|
|
49
|
+
if isinstance(usage, dict):
|
|
50
|
+
return usage.get("prompt_tokens"), usage.get("completion_tokens")
|
|
51
|
+
return (
|
|
52
|
+
getattr(usage, "prompt_tokens", None),
|
|
53
|
+
getattr(usage, "completion_tokens", None),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _post_obs(
|
|
58
|
+
transport: ObservationTransport,
|
|
59
|
+
model: str,
|
|
60
|
+
operation: str,
|
|
61
|
+
tenant_id: str | None,
|
|
62
|
+
environment: str | None,
|
|
63
|
+
connection_id: str | None,
|
|
64
|
+
correlation_id: str,
|
|
65
|
+
request_at: str,
|
|
66
|
+
input_tokens: int | None,
|
|
67
|
+
output_tokens: int | None,
|
|
68
|
+
status: str,
|
|
69
|
+
error_message: str | None,
|
|
70
|
+
) -> None:
|
|
71
|
+
obs: dict = {
|
|
72
|
+
"sdk_version": _SDK_VERSION,
|
|
73
|
+
"provider": "openai",
|
|
74
|
+
"model": model,
|
|
75
|
+
"operation": operation,
|
|
76
|
+
"request_at": request_at,
|
|
77
|
+
"response_at": _now_iso(),
|
|
78
|
+
"input_tokens": input_tokens,
|
|
79
|
+
"output_tokens": output_tokens,
|
|
80
|
+
"cost_usd_cents": None,
|
|
81
|
+
"status": status,
|
|
82
|
+
"error_message": error_message,
|
|
83
|
+
"correlation_id": correlation_id,
|
|
84
|
+
"tenant_id": tenant_id,
|
|
85
|
+
**({"environment": environment} if environment is not None else {}),
|
|
86
|
+
**({"connection_id": connection_id} if connection_id is not None else {}),
|
|
87
|
+
}
|
|
88
|
+
try:
|
|
89
|
+
transport.post(obs)
|
|
90
|
+
except Exception: # noqa: BLE001
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _wrap_method(
|
|
95
|
+
bound_method: Any,
|
|
96
|
+
*,
|
|
97
|
+
operation: str,
|
|
98
|
+
transport: ObservationTransport,
|
|
99
|
+
tenant_id: str | None,
|
|
100
|
+
environment: str | None,
|
|
101
|
+
connection_id: str | None,
|
|
102
|
+
applied_mode: bool = False,
|
|
103
|
+
proxy_url: Optional[str] = None,
|
|
104
|
+
client_ref: Optional[Any] = None,
|
|
105
|
+
retry: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
106
|
+
timeout_s: float = 60.0,
|
|
107
|
+
on_lifecycle_event: Optional[LifecycleEventCallback] = None,
|
|
108
|
+
fail_open_enabled: bool = True,
|
|
109
|
+
circuit: Optional[CircuitBreaker] = None,
|
|
110
|
+
) -> Any:
|
|
111
|
+
@functools.wraps(bound_method)
|
|
112
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
113
|
+
request_at = _now_iso()
|
|
114
|
+
model = kwargs.get("model") or (args[0] if args else "unknown")
|
|
115
|
+
correlation_id = str(uuid.uuid4())
|
|
116
|
+
|
|
117
|
+
# Applied-mode path: route chat/completions through the proxy-client.
|
|
118
|
+
if applied_mode and proxy_url and operation == "chat.completions":
|
|
119
|
+
provider_url = openai_provider_base_url(client_ref) if client_ref else "https://api.openai.com"
|
|
120
|
+
provider_auth = openai_provider_auth(client_ref) if client_ref else ""
|
|
121
|
+
|
|
122
|
+
body: dict[str, Any] = dict(kwargs)
|
|
123
|
+
body["model"] = str(model)
|
|
124
|
+
|
|
125
|
+
circuit_was_open = circuit is not None and circuit.should_bypass()
|
|
126
|
+
proxy_failed = False
|
|
127
|
+
proxy_error: Optional[Exception] = None
|
|
128
|
+
proxy_result: Optional[Any] = None
|
|
129
|
+
|
|
130
|
+
if not circuit_was_open:
|
|
131
|
+
try:
|
|
132
|
+
bearer_token = transport._get_token()
|
|
133
|
+
proxy_result = proxy_request(
|
|
134
|
+
proxy_url=proxy_url,
|
|
135
|
+
bearer_token=bearer_token,
|
|
136
|
+
provider_url=provider_url,
|
|
137
|
+
provider_auth=provider_auth,
|
|
138
|
+
body=body,
|
|
139
|
+
model=str(model),
|
|
140
|
+
provider="openai",
|
|
141
|
+
operation=operation,
|
|
142
|
+
correlation_id=correlation_id,
|
|
143
|
+
environment=environment,
|
|
144
|
+
retry=retry,
|
|
145
|
+
timeout_s=timeout_s,
|
|
146
|
+
on_lifecycle_event=on_lifecycle_event,
|
|
147
|
+
circuit=circuit,
|
|
148
|
+
)
|
|
149
|
+
except Exception as exc: # noqa: BLE001
|
|
150
|
+
proxy_failed = True
|
|
151
|
+
proxy_error = exc
|
|
152
|
+
else:
|
|
153
|
+
proxy_failed = True
|
|
154
|
+
|
|
155
|
+
if proxy_failed or circuit_was_open:
|
|
156
|
+
if not fail_open_enabled:
|
|
157
|
+
tc_err = (
|
|
158
|
+
proxy_error
|
|
159
|
+
if isinstance(proxy_error, TensorCostProxyError)
|
|
160
|
+
else TensorCostProxyError(
|
|
161
|
+
str(proxy_error) if proxy_error else "proxy unavailable",
|
|
162
|
+
root_cause=proxy_error if isinstance(proxy_error, Exception) else None,
|
|
163
|
+
attempt=1,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
_post_obs(
|
|
167
|
+
transport, str(model), operation, tenant_id, environment,
|
|
168
|
+
connection_id, correlation_id, request_at, None, None,
|
|
169
|
+
"error", f"{type(tc_err).__name__}: {tc_err}",
|
|
170
|
+
)
|
|
171
|
+
raise tc_err
|
|
172
|
+
|
|
173
|
+
# Fail-open: emit fallback event and call provider directly.
|
|
174
|
+
emit_event(
|
|
175
|
+
on_lifecycle_event,
|
|
176
|
+
OnFallbackEvent(
|
|
177
|
+
kind="on_fallback",
|
|
178
|
+
provider="openai",
|
|
179
|
+
model=str(model),
|
|
180
|
+
operation=operation,
|
|
181
|
+
attempt_number=1,
|
|
182
|
+
elapsed_ms=0.0,
|
|
183
|
+
consecutive_failures=circuit.failures if circuit else 0,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
try:
|
|
187
|
+
direct_response = bound_method(*args, **kwargs)
|
|
188
|
+
input_tokens, output_tokens = _extract_usage(direct_response)
|
|
189
|
+
_post_obs(
|
|
190
|
+
transport, str(model), operation, tenant_id, environment,
|
|
191
|
+
connection_id, correlation_id, request_at, input_tokens,
|
|
192
|
+
output_tokens, "success", None,
|
|
193
|
+
)
|
|
194
|
+
return direct_response
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
_post_obs(
|
|
197
|
+
transport, str(model), operation, tenant_id, environment,
|
|
198
|
+
connection_id, correlation_id, request_at, None, None,
|
|
199
|
+
"error", f"{type(exc).__name__}: {exc}",
|
|
200
|
+
)
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
# Proxy success.
|
|
204
|
+
resp_dict = proxy_result if isinstance(proxy_result, dict) else {}
|
|
205
|
+
usage = resp_dict.get("usage") if isinstance(resp_dict, dict) else None
|
|
206
|
+
input_tokens = usage.get("prompt_tokens") if isinstance(usage, dict) else None
|
|
207
|
+
output_tokens = usage.get("completion_tokens") if isinstance(usage, dict) else None
|
|
208
|
+
_post_obs(
|
|
209
|
+
transport, str(model), operation, tenant_id, environment,
|
|
210
|
+
connection_id, correlation_id, request_at, input_tokens,
|
|
211
|
+
output_tokens, "success", None,
|
|
212
|
+
)
|
|
213
|
+
return proxy_result
|
|
214
|
+
|
|
215
|
+
# Observe-only path (applied_mode=False or non-chat operation).
|
|
216
|
+
try:
|
|
217
|
+
response = bound_method(*args, **kwargs)
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
_post_obs(
|
|
220
|
+
transport, str(model), operation, tenant_id, environment,
|
|
221
|
+
connection_id, correlation_id, request_at, None, None,
|
|
222
|
+
"error", f"{type(exc).__name__}: {exc}",
|
|
223
|
+
)
|
|
224
|
+
raise
|
|
225
|
+
|
|
226
|
+
input_tokens, output_tokens = _extract_usage(response)
|
|
227
|
+
_post_obs(
|
|
228
|
+
transport, str(model), operation, tenant_id, environment,
|
|
229
|
+
connection_id, correlation_id, request_at, input_tokens,
|
|
230
|
+
output_tokens, "success", None,
|
|
231
|
+
)
|
|
232
|
+
return response
|
|
233
|
+
|
|
234
|
+
setattr(wrapper, "__tensorcost_wrapped__", True)
|
|
235
|
+
return wrapper
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def install(
|
|
239
|
+
client: Any,
|
|
240
|
+
transport: ObservationTransport,
|
|
241
|
+
tenant_id: str | None,
|
|
242
|
+
environment: str | None,
|
|
243
|
+
connection_id: str | None,
|
|
244
|
+
applied_mode: bool = False,
|
|
245
|
+
proxy_url: Optional[str] = None,
|
|
246
|
+
retry: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
247
|
+
timeout_s: float = 60.0,
|
|
248
|
+
on_lifecycle_event: Optional[LifecycleEventCallback] = None,
|
|
249
|
+
fail_open_enabled: bool = True,
|
|
250
|
+
circuit: Optional[CircuitBreaker] = None,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Monkey-patch the OpenAI client's ``create`` methods in place."""
|
|
253
|
+
chat = getattr(client, "chat", None)
|
|
254
|
+
completions = getattr(chat, "completions", None) if chat is not None else None
|
|
255
|
+
if completions is not None and callable(getattr(completions, "create", None)):
|
|
256
|
+
original = completions.create
|
|
257
|
+
if not getattr(original, "__tensorcost_wrapped__", False):
|
|
258
|
+
completions.create = _wrap_method(
|
|
259
|
+
original,
|
|
260
|
+
operation="chat.completions",
|
|
261
|
+
transport=transport,
|
|
262
|
+
tenant_id=tenant_id,
|
|
263
|
+
environment=environment,
|
|
264
|
+
connection_id=connection_id,
|
|
265
|
+
applied_mode=applied_mode,
|
|
266
|
+
proxy_url=proxy_url,
|
|
267
|
+
client_ref=client,
|
|
268
|
+
retry=retry,
|
|
269
|
+
timeout_s=timeout_s,
|
|
270
|
+
on_lifecycle_event=on_lifecycle_event,
|
|
271
|
+
fail_open_enabled=fail_open_enabled,
|
|
272
|
+
circuit=circuit,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
legacy = getattr(client, "completions", None)
|
|
276
|
+
if legacy is not None and callable(getattr(legacy, "create", None)):
|
|
277
|
+
original = legacy.create
|
|
278
|
+
if not getattr(original, "__tensorcost_wrapped__", False):
|
|
279
|
+
legacy.create = _wrap_method(
|
|
280
|
+
original,
|
|
281
|
+
operation="completions",
|
|
282
|
+
transport=transport,
|
|
283
|
+
tenant_id=tenant_id,
|
|
284
|
+
environment=environment,
|
|
285
|
+
connection_id=connection_id,
|
|
286
|
+
# Legacy completions endpoint not in scope for applied-mode v1.
|
|
287
|
+
applied_mode=False,
|
|
288
|
+
proxy_url=None,
|
|
289
|
+
client_ref=client,
|
|
290
|
+
retry=retry,
|
|
291
|
+
timeout_s=timeout_s,
|
|
292
|
+
on_lifecycle_event=on_lifecycle_event,
|
|
293
|
+
fail_open_enabled=fail_open_enabled,
|
|
294
|
+
circuit=circuit,
|
|
295
|
+
)
|