cavisson-pythonagent 0.0.1__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.
- cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
- cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
- cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
- cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
- cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
- pythonagent/__init__.py +22 -0
- pythonagent/agent/__init__.py +219 -0
- pythonagent/agent/internal/__init__.py +1 -0
- pythonagent/agent/internal/agent.py +1651 -0
- pythonagent/agent/internal/framesinfo.py +152 -0
- pythonagent/agent/internal/heap_dump.py +39 -0
- pythonagent/agent/internal/intercept_module.py +70 -0
- pythonagent/agent/internal/logs.py +122 -0
- pythonagent/agent/internal/metadata/__init__.py +0 -0
- pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
- pythonagent/agent/internal/proc_compat.py +75 -0
- pythonagent/agent/internal/profile.py +47 -0
- pythonagent/agent/internal/provider.py +83 -0
- pythonagent/agent/internal/thread_dump.py +85 -0
- pythonagent/agent/internal/udp.py +245 -0
- pythonagent/agent/internal/udp_message.py +800 -0
- pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
- pythonagent/agent/probes/Instrumentation/find.py +243 -0
- pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
- pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
- pythonagent/agent/probes/Instrumentation/parser.py +129 -0
- pythonagent/agent/probes/__init__.py +219 -0
- pythonagent/agent/probes/base.py +303 -0
- pythonagent/agent/probes/cache/__init__.py +51 -0
- pythonagent/agent/probes/cache/redis.py +119 -0
- pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
- pythonagent/agent/probes/coroutines/__init__.py +1 -0
- pythonagent/agent/probes/coroutines/asyncio.py +63 -0
- pythonagent/agent/probes/elasticdb/__init__.py +7 -0
- pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
- pythonagent/agent/probes/frameworks/__init__.py +27 -0
- pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
- pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
- pythonagent/agent/probes/frameworks/aisess.py +151 -0
- pythonagent/agent/probes/frameworks/asgi.py +340 -0
- pythonagent/agent/probes/frameworks/bottle.py +27 -0
- pythonagent/agent/probes/frameworks/cherry.py +25 -0
- pythonagent/agent/probes/frameworks/django.py +128 -0
- pythonagent/agent/probes/frameworks/falcon.py +21 -0
- pythonagent/agent/probes/frameworks/fastapi.py +35 -0
- pythonagent/agent/probes/frameworks/flask.py +30 -0
- pythonagent/agent/probes/frameworks/pyramid.py +56 -0
- pythonagent/agent/probes/frameworks/test.py +108 -0
- pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
- pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
- pythonagent/agent/probes/frameworks/wsgi.py +353 -0
- pythonagent/agent/probes/grpc/__init__.py +76 -0
- pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
- pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
- pythonagent/agent/probes/havoc/__init__.py +0 -0
- pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
- pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
- pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
- pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
- pythonagent/agent/probes/http/__init__.py +49 -0
- pythonagent/agent/probes/http/aiohttp_client.py +59 -0
- pythonagent/agent/probes/http/boto.py +12 -0
- pythonagent/agent/probes/http/httplib.py +110 -0
- pythonagent/agent/probes/http/httpx_client.py +116 -0
- pythonagent/agent/probes/http/requests.py +15 -0
- pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
- pythonagent/agent/probes/http/urllib3.py +16 -0
- pythonagent/agent/probes/langchain/__init__.py +21 -0
- pythonagent/agent/probes/langchain/base_tool.py +95 -0
- pythonagent/agent/probes/langchain/langchain_community.py +136 -0
- pythonagent/agent/probes/langchain/langchain_core.py +32 -0
- pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
- pythonagent/agent/probes/logging/__init__.py +106 -0
- pythonagent/agent/probes/message_brokers/__init__.py +4 -0
- pythonagent/agent/probes/message_brokers/pika.py +126 -0
- pythonagent/agent/probes/mongodb/__init__.py +6 -0
- pythonagent/agent/probes/mongodb/pymongo.py +286 -0
- pythonagent/agent/probes/openai/__init__.py +3 -0
- pythonagent/agent/probes/openai/openai.py +797 -0
- pythonagent/agent/probes/span.py +101 -0
- pythonagent/agent/probes/sql/__init__.py +13 -0
- pythonagent/agent/probes/sql/botocores3.py +51 -0
- pythonagent/agent/probes/sql/dbapi.py +285 -0
- pythonagent/agent/probes/sql/dynamodb.py +90 -0
- pythonagent/agent/probes/sql/mysql_connector.py +24 -0
- pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
- pythonagent/agent/probes/sql/mysqldb.py +43 -0
- pythonagent/agent/probes/sql/psycopg2.py +174 -0
- pythonagent/agent/probes/sql/pymysql.py +25 -0
- pythonagent/bootstrap/__init__.py +0 -0
- pythonagent/bootstrap/cav_gunicorn.py +26 -0
- pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
- pythonagent/bootstrap/run.py +47 -0
- pythonagent/bootstrap/sitecustomize.py +287 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
- pythonagent/config.py +279 -0
- pythonagent/find.py +72 -0
- pythonagent/find_mod_cls_name.py +54 -0
- pythonagent/lang.py +131 -0
- pythonagent/lib.py +91 -0
- pythonagent/main/__init__.py +0 -0
- pythonagent/main/pytrace/__init__.py +79 -0
- pythonagent/main/pytrace/commands/__init__.py +0 -0
- pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
- pythonagent/main/pytrace/commands/run.py +401 -0
- pythonagent/main/pytrace/pytrace.py +133 -0
- pythonagent/main/wsgi.py +6 -0
- pythonagent/main.py +27 -0
- pythonagent/run.py +46 -0
- pythonagent/sqins.py +8 -0
- pythonagent/test.py +73 -0
- pythonagent/utils.py +168 -0
- pythonagent/vendor/__init__.py +0 -0
- pythonagent/vendor/pympler/__init__.py +1 -0
- pythonagent/vendor/pympler/asizeof.py +2810 -0
- pythonagent/vendor/pympler/charts.py +62 -0
- pythonagent/vendor/pympler/classtracker.py +590 -0
- pythonagent/vendor/pympler/classtracker_stats.py +780 -0
- pythonagent/vendor/pympler/garbagegraph.py +80 -0
- pythonagent/vendor/pympler/mprofile.py +97 -0
- pythonagent/vendor/pympler/muppy.py +275 -0
- pythonagent/vendor/pympler/panels.py +115 -0
- pythonagent/vendor/pympler/process.py +238 -0
- pythonagent/vendor/pympler/py.typed +0 -0
- pythonagent/vendor/pympler/refbrowser.py +451 -0
- pythonagent/vendor/pympler/refgraph.py +350 -0
- pythonagent/vendor/pympler/summary.py +321 -0
- pythonagent/vendor/pympler/tracker.py +267 -0
- pythonagent/vendor/pympler/util/__init__.py +0 -0
- pythonagent/vendor/pympler/util/bottle.py +3809 -0
- pythonagent/vendor/pympler/util/compat.py +23 -0
- pythonagent/vendor/pympler/util/stringutils.py +77 -0
- pythonagent/vendor/pympler/web.py +346 -0
- pythonagent/vendor/werkzeug/__init__.py +20 -0
- pythonagent/vendor/werkzeug/_compat.py +228 -0
- pythonagent/vendor/werkzeug/_internal.py +473 -0
- pythonagent/vendor/werkzeug/_reloader.py +341 -0
- pythonagent/vendor/werkzeug/datastructures.py +3120 -0
- pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
- pythonagent/vendor/werkzeug/debug/console.py +218 -0
- pythonagent/vendor/werkzeug/debug/repr.py +297 -0
- pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
- pythonagent/vendor/werkzeug/exceptions.py +829 -0
- pythonagent/vendor/werkzeug/filesystem.py +64 -0
- pythonagent/vendor/werkzeug/formparser.py +584 -0
- pythonagent/vendor/werkzeug/http.py +1307 -0
- pythonagent/vendor/werkzeug/local.py +420 -0
- pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
- pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
- pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
- pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
- pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
- pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
- pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
- pythonagent/vendor/werkzeug/posixemulation.py +117 -0
- pythonagent/vendor/werkzeug/routing.py +2210 -0
- pythonagent/vendor/werkzeug/security.py +249 -0
- pythonagent/vendor/werkzeug/serving.py +1117 -0
- pythonagent/vendor/werkzeug/test.py +1123 -0
- pythonagent/vendor/werkzeug/testapp.py +241 -0
- pythonagent/vendor/werkzeug/urls.py +1138 -0
- pythonagent/vendor/werkzeug/useragents.py +202 -0
- pythonagent/vendor/werkzeug/utils.py +778 -0
- pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
- pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
- pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
- pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
- pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
- pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
- pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
- pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
- pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
- pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
- pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
- pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
- pythonagent/vendor/werkzeug/wsgi.py +1000 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
from ..base import BaseInterceptor, ExitCallInterceptor
|
|
2
|
+
from ..span import LLMSpan, EmbeddingSpan
|
|
3
|
+
|
|
4
|
+
from pythonagent.utils import get_validated_duration, get_current_timestamp_in_us, build_host_name_str, split_fp_topo
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Instrumentation wrappers for the OpenAI Python SDK (v1.x).
|
|
9
|
+
|
|
10
|
+
Covers:
|
|
11
|
+
- Sync chat completions (non-streaming + streaming)
|
|
12
|
+
- Async chat completions (non-streaming + streaming)
|
|
13
|
+
- Legacy (non-chat) completions, sync + async
|
|
14
|
+
- Embeddings, sync + async
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Shared helpers
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def _safe_usage_metrics(usage):
|
|
23
|
+
"""Build a metrics dict from a usage object, guarding every field for None."""
|
|
24
|
+
if usage is None:
|
|
25
|
+
return {
|
|
26
|
+
"prompt_tokens": None,
|
|
27
|
+
"completion_tokens": None,
|
|
28
|
+
"total_tokens": None,
|
|
29
|
+
"prompt_tokens_details": {"prompt_audio_tokens": None, "prompt_cached_tokens": None},
|
|
30
|
+
"completion_tokens_details": {
|
|
31
|
+
"completion_accepted_prediction_tokens": None,
|
|
32
|
+
"completion_rejected_prediction_tokens": None,
|
|
33
|
+
"completion_audio_tokens": None,
|
|
34
|
+
"completion_reasoning_tokens": None,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
ptd = getattr(usage, "prompt_tokens_details", None)
|
|
38
|
+
ctd = getattr(usage, "completion_tokens_details", None)
|
|
39
|
+
return {
|
|
40
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", None),
|
|
41
|
+
"completion_tokens": getattr(usage, "completion_tokens", None),
|
|
42
|
+
"total_tokens": getattr(usage, "total_tokens", None),
|
|
43
|
+
"prompt_tokens_details": {
|
|
44
|
+
"prompt_audio_tokens": getattr(ptd, "audio_tokens", None) if ptd else None,
|
|
45
|
+
"prompt_cached_tokens": getattr(ptd, "cached_tokens", None) if ptd else None,
|
|
46
|
+
},
|
|
47
|
+
"completion_tokens_details": {
|
|
48
|
+
"completion_accepted_prediction_tokens": getattr(ctd, "accepted_prediction_tokens", None) if ctd else None,
|
|
49
|
+
"completion_rejected_prediction_tokens": getattr(ctd, "rejected_prediction_tokens", None) if ctd else None,
|
|
50
|
+
"completion_audio_tokens": getattr(ctd, "audio_tokens", None) if ctd else None,
|
|
51
|
+
"completion_reasoning_tokens": getattr(ctd, "reasoning_tokens", None) if ctd else None,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_messages_input(args, kwargs):
|
|
57
|
+
"""Return messages list, handling both positional and keyword argument (Gap 5)."""
|
|
58
|
+
return kwargs.get("messages") or (args[0] if args else [])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_model_name(args, kwargs):
|
|
62
|
+
"""Best-effort extraction of the requested model name before the call is made
|
|
63
|
+
(ported from the old single-entry-point file's _get_model_name)."""
|
|
64
|
+
model_name = kwargs.get("model")
|
|
65
|
+
if not model_name and args:
|
|
66
|
+
model_name = getattr(args[0], "model", None) or getattr(args[0], "model_name", None)
|
|
67
|
+
return model_name or "default"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _build_chat_output(result):
|
|
71
|
+
"""
|
|
72
|
+
Extract output from all choices of a ChatCompletion response.
|
|
73
|
+
Returns a single dict for n=1, a list for n>1 (Gap 8).
|
|
74
|
+
"""
|
|
75
|
+
choices = getattr(result, "choices", None) or []
|
|
76
|
+
outputs = []
|
|
77
|
+
for choice in choices:
|
|
78
|
+
msg = getattr(choice, "message", None)
|
|
79
|
+
if msg is None:
|
|
80
|
+
continue
|
|
81
|
+
outputs.append({
|
|
82
|
+
"content": getattr(msg, "content", None),
|
|
83
|
+
"role": getattr(msg, "role", None),
|
|
84
|
+
"tool_calls": str(getattr(msg, "tool_calls", None)),
|
|
85
|
+
"completion_id": getattr(result, "id", None),
|
|
86
|
+
"finish_reason": getattr(choice, "finish_reason", None),
|
|
87
|
+
"refusal": getattr(msg, "refusal", None),
|
|
88
|
+
"annotations": getattr(msg, "annotations", None),
|
|
89
|
+
"audio": getattr(msg, "audio", None),
|
|
90
|
+
"function_call": str(getattr(msg, "function_call", None)),
|
|
91
|
+
})
|
|
92
|
+
if len(outputs) == 1:
|
|
93
|
+
return outputs[0]
|
|
94
|
+
return outputs if outputs else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _accumulate_stream_chunks(chunks):
|
|
98
|
+
"""
|
|
99
|
+
Merge ChatCompletionChunk list into aggregated output fields.
|
|
100
|
+
Returns (content_by_index, model_name, completion_id, finish_reason, usage).
|
|
101
|
+
"""
|
|
102
|
+
content_by_index = {}
|
|
103
|
+
model_name = "default"
|
|
104
|
+
completion_id = None
|
|
105
|
+
finish_reason = None
|
|
106
|
+
usage = None
|
|
107
|
+
|
|
108
|
+
for chunk in chunks:
|
|
109
|
+
model_name = getattr(chunk, "model", None) or model_name
|
|
110
|
+
completion_id = getattr(chunk, "id", None) or completion_id
|
|
111
|
+
# usage arrives on the last chunk when stream_options={"include_usage": True}
|
|
112
|
+
chunk_usage = getattr(chunk, "usage", None)
|
|
113
|
+
if chunk_usage is not None:
|
|
114
|
+
usage = chunk_usage
|
|
115
|
+
for choice in (getattr(chunk, "choices", None) or []):
|
|
116
|
+
idx = getattr(choice, "index", 0)
|
|
117
|
+
delta = getattr(choice, "delta", None)
|
|
118
|
+
if delta:
|
|
119
|
+
text = getattr(delta, "content", None)
|
|
120
|
+
if text:
|
|
121
|
+
content_by_index[idx] = content_by_index.get(idx, "") + text
|
|
122
|
+
fr = getattr(choice, "finish_reason", None)
|
|
123
|
+
if fr:
|
|
124
|
+
finish_reason = fr
|
|
125
|
+
|
|
126
|
+
return content_by_index, model_name, completion_id, finish_reason, usage
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _build_stream_output(content_by_index, completion_id, finish_reason):
|
|
130
|
+
"""Build output dict(s) from accumulated streaming content."""
|
|
131
|
+
indices = sorted(content_by_index.keys())
|
|
132
|
+
if not indices:
|
|
133
|
+
return {"content": None, "completion_id": completion_id, "finish_reason": finish_reason,
|
|
134
|
+
"role": "assistant"}
|
|
135
|
+
outputs = [
|
|
136
|
+
{
|
|
137
|
+
"content": content_by_index.get(i),
|
|
138
|
+
"completion_id": completion_id,
|
|
139
|
+
"finish_reason": finish_reason,
|
|
140
|
+
"role": "assistant",
|
|
141
|
+
}
|
|
142
|
+
for i in indices
|
|
143
|
+
]
|
|
144
|
+
return outputs[0] if len(outputs) == 1 else outputs
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _split_fp(context):
|
|
148
|
+
"""Safely split fp_topo from context; returns 7-tuple of Nones on failure."""
|
|
149
|
+
try:
|
|
150
|
+
if context:
|
|
151
|
+
return split_fp_topo(context.fp_topo)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
return None, None, None, None, None, None, None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _record_llm_span(interceptor, bt, fqm, start_time, duration, context,
|
|
158
|
+
args, kwargs, model_name, output, usage):
|
|
159
|
+
"""Create and send an LLMSpan if nf_enabled."""
|
|
160
|
+
if not interceptor.agent.nf_enabled:
|
|
161
|
+
return
|
|
162
|
+
metrics = _safe_usage_metrics(usage)
|
|
163
|
+
test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp = _split_fp(context)
|
|
164
|
+
meta = {
|
|
165
|
+
"span_kind": "llm",
|
|
166
|
+
"input": _get_messages_input(args, kwargs),
|
|
167
|
+
"output": output,
|
|
168
|
+
"model_name": model_name,
|
|
169
|
+
"model_provider": "OpenAI",
|
|
170
|
+
"metadata": kwargs.get("metadata", None),
|
|
171
|
+
}
|
|
172
|
+
llm_span = LLMSpan(
|
|
173
|
+
fqm, start_time, duration, "ok", meta, metrics,
|
|
174
|
+
test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp
|
|
175
|
+
)
|
|
176
|
+
if interceptor.agent.logger.isEnabledFor(logging.INFO):
|
|
177
|
+
interceptor.agent.logger.info("openai llm sending llm span output={}".format(output))
|
|
178
|
+
interceptor.agent.dump_log_message(bt, llm_span.create_span_json())
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _record_call_success(interceptor, bt, exit_call, start_time, fqm, context,
|
|
182
|
+
username, args, kwargs, model_name, output, usage, label="OpenAI LLM"):
|
|
183
|
+
"""Compute duration, call db_call_end, and send LLMSpan."""
|
|
184
|
+
end_time = get_current_timestamp_in_us()
|
|
185
|
+
duration = get_validated_duration(start_time, end_time, label)
|
|
186
|
+
host_name_str = build_host_name_str("NA", "NA", "NA", model_name, username)
|
|
187
|
+
if context:
|
|
188
|
+
context.host_str = host_name_str
|
|
189
|
+
context.status_code = 200
|
|
190
|
+
context.db_callout_time = 0
|
|
191
|
+
if interceptor.agent.logger.isEnabledFor(logging.INFO):
|
|
192
|
+
interceptor.agent.logger.info(
|
|
193
|
+
"openai llm db call end bt={} exit_call={} duration={} fqm={} model={}".format(
|
|
194
|
+
bt, exit_call, duration, fqm, model_name
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
interceptor.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
|
|
198
|
+
_record_llm_span(interceptor, bt, fqm, start_time, duration, context,
|
|
199
|
+
args, kwargs, model_name, output, usage)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _record_call_error(interceptor, bt, exit_call, start_time, fqm, context, username,
|
|
203
|
+
label="OpenAI LLM"):
|
|
204
|
+
"""Call db_call_end on the exception path with status 500."""
|
|
205
|
+
end_time = get_current_timestamp_in_us()
|
|
206
|
+
duration = get_validated_duration(start_time, end_time, label)
|
|
207
|
+
host_name_str = build_host_name_str("NA", "NA", "LLM", "OpenAI_default", username)
|
|
208
|
+
if context:
|
|
209
|
+
context.host_str = host_name_str
|
|
210
|
+
context.status_code = 500
|
|
211
|
+
context.db_callout_time = 0
|
|
212
|
+
if interceptor.agent.logger.isEnabledFor(logging.INFO):
|
|
213
|
+
interceptor.agent.logger.info(
|
|
214
|
+
"openai llm db call end (error) bt={} exit_call={} fqm={}".format(bt, exit_call, fqm)
|
|
215
|
+
)
|
|
216
|
+
interceptor.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# Stream wrappers
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
class SyncStreamWrapper:
|
|
224
|
+
"""
|
|
225
|
+
Transparent wrapper around openai.Stream[ChatCompletionChunk].
|
|
226
|
+
Accumulates chunks and fires on_finish after the last chunk is consumed
|
|
227
|
+
or the iterator is closed early (via finally). (Gap 2)
|
|
228
|
+
"""
|
|
229
|
+
def __init__(self, stream, on_finish):
|
|
230
|
+
self._stream = stream
|
|
231
|
+
self._on_finish = on_finish
|
|
232
|
+
self._chunks = []
|
|
233
|
+
self._finished = False
|
|
234
|
+
|
|
235
|
+
def _fire_finish(self):
|
|
236
|
+
if not self._finished:
|
|
237
|
+
self._finished = True
|
|
238
|
+
self._on_finish(self._chunks)
|
|
239
|
+
|
|
240
|
+
def __iter__(self):
|
|
241
|
+
try:
|
|
242
|
+
for chunk in self._stream:
|
|
243
|
+
self._chunks.append(chunk)
|
|
244
|
+
yield chunk
|
|
245
|
+
finally:
|
|
246
|
+
self._fire_finish()
|
|
247
|
+
|
|
248
|
+
def __enter__(self):
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def __exit__(self, *args):
|
|
252
|
+
self._fire_finish()
|
|
253
|
+
aexit = getattr(self._stream, "__exit__", None)
|
|
254
|
+
if aexit:
|
|
255
|
+
return aexit(*args)
|
|
256
|
+
|
|
257
|
+
def __getattr__(self, name):
|
|
258
|
+
return getattr(self._stream, name)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class AsyncStreamWrapper:
|
|
262
|
+
"""
|
|
263
|
+
Transparent wrapper around openai.AsyncStream[ChatCompletionChunk].
|
|
264
|
+
Fires on_finish (synchronous SDK call) after the last chunk or early close. (Gap 3)
|
|
265
|
+
"""
|
|
266
|
+
def __init__(self, stream, on_finish):
|
|
267
|
+
self._stream = stream
|
|
268
|
+
self._on_finish = on_finish
|
|
269
|
+
self._chunks = []
|
|
270
|
+
self._finished = False
|
|
271
|
+
self._iterator = None
|
|
272
|
+
|
|
273
|
+
def _fire_finish(self):
|
|
274
|
+
if not self._finished:
|
|
275
|
+
self._finished = True
|
|
276
|
+
self._on_finish(self._chunks)
|
|
277
|
+
|
|
278
|
+
def __aiter__(self):
|
|
279
|
+
self._iterator = self._stream.__aiter__()
|
|
280
|
+
return self
|
|
281
|
+
|
|
282
|
+
async def __anext__(self):
|
|
283
|
+
try:
|
|
284
|
+
chunk = await self._iterator.__anext__()
|
|
285
|
+
self._chunks.append(chunk)
|
|
286
|
+
return chunk
|
|
287
|
+
except StopAsyncIteration:
|
|
288
|
+
self._fire_finish()
|
|
289
|
+
raise
|
|
290
|
+
|
|
291
|
+
async def __aenter__(self):
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
async def __aexit__(self, *args):
|
|
295
|
+
self._fire_finish()
|
|
296
|
+
aexit = getattr(self._stream, "__aexit__", None)
|
|
297
|
+
if aexit:
|
|
298
|
+
await aexit(*args)
|
|
299
|
+
|
|
300
|
+
def __getattr__(self, name):
|
|
301
|
+
return getattr(self._stream, name)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Internal: build streaming on_finish closure
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def _make_stream_on_finish(interceptor, bt, exit_call, start_time, fqm,
|
|
309
|
+
context, username, args, kwargs):
|
|
310
|
+
"""Return a closure suitable for SyncStreamWrapper / AsyncStreamWrapper."""
|
|
311
|
+
def on_finish(chunks):
|
|
312
|
+
try:
|
|
313
|
+
content_by_idx, model_name, completion_id, finish_reason, usage = \
|
|
314
|
+
_accumulate_stream_chunks(chunks)
|
|
315
|
+
output = _build_stream_output(content_by_idx, completion_id, finish_reason)
|
|
316
|
+
_record_call_success(
|
|
317
|
+
interceptor, bt, exit_call, start_time, fqm, context,
|
|
318
|
+
username, args, kwargs, model_name, output, usage
|
|
319
|
+
)
|
|
320
|
+
except Exception as e:
|
|
321
|
+
interceptor.agent.logger.exception(
|
|
322
|
+
"openai stream on_finish error: {}".format(e)
|
|
323
|
+
)
|
|
324
|
+
return on_finish
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Chat Completions — Sync (existing, with all gap fixes applied)
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
class OpenAIInterceptor(ExitCallInterceptor):
|
|
332
|
+
def wrap_create(self, create, *args, **kwargs):
|
|
333
|
+
start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
|
|
334
|
+
try:
|
|
335
|
+
bt = self.bt
|
|
336
|
+
username = "NA"
|
|
337
|
+
host, port, query, query_params = "NA", "NA", "NA", None
|
|
338
|
+
model_name = _get_model_name(args, kwargs)
|
|
339
|
+
host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
|
|
340
|
+
fqm = "{}.{}".format(create.__module__, create.__qualname__)
|
|
341
|
+
context = self.agent.get_transaction_context()
|
|
342
|
+
if context:
|
|
343
|
+
context.entry_point_fqm = fqm
|
|
344
|
+
context.host_str = host_name_str
|
|
345
|
+
start_time = get_current_timestamp_in_us()
|
|
346
|
+
if self.agent.logger.isEnabledFor(logging.INFO):
|
|
347
|
+
self.agent.logger.info(
|
|
348
|
+
"openai llm db call begin bt={} start_time={}".format(bt, start_time)
|
|
349
|
+
)
|
|
350
|
+
exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
|
|
353
|
+
if isinstance(e, NDHavocException):
|
|
354
|
+
raise e
|
|
355
|
+
self.agent.logger.exception("openai llm db call begin: {}".format(e))
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
result = create(*args, **kwargs)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
self.agent.logger.exception("openai llm create error: {}".format(e))
|
|
361
|
+
try:
|
|
362
|
+
_record_call_error(self, bt, exit_call, start_time, fqm, context, username)
|
|
363
|
+
except Exception as inner:
|
|
364
|
+
self.agent.logger.exception("openai llm db call end (error): {}".format(inner))
|
|
365
|
+
raise
|
|
366
|
+
|
|
367
|
+
# Streaming path — wrap stream, defer telemetry until consumed (Gap 2)
|
|
368
|
+
if kwargs.get("stream", False):
|
|
369
|
+
return SyncStreamWrapper(
|
|
370
|
+
result,
|
|
371
|
+
_make_stream_on_finish(self, bt, exit_call, start_time, fqm,
|
|
372
|
+
context, username, args, kwargs)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Non-streaming path
|
|
376
|
+
try:
|
|
377
|
+
model_name = getattr(result, "model", None) or getattr(result, "model_name", None) or "default"
|
|
378
|
+
output = _build_chat_output(result)
|
|
379
|
+
usage = getattr(result, "usage", None)
|
|
380
|
+
_record_call_success(
|
|
381
|
+
self, bt, exit_call, start_time, fqm, context,
|
|
382
|
+
username, args, kwargs, model_name, output, usage
|
|
383
|
+
)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
self.agent.logger.exception("openai llm db call end: {}".format(e))
|
|
386
|
+
|
|
387
|
+
return result
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Chat Completions — Async (Gap 1 + Gap 3)
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
class AsyncOpenAIInterceptor(ExitCallInterceptor):
|
|
395
|
+
async def wrap_create(self, create, *args, **kwargs):
|
|
396
|
+
start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
|
|
397
|
+
try:
|
|
398
|
+
bt = self.bt
|
|
399
|
+
username = "NA"
|
|
400
|
+
host, port, query, query_params = "NA", "NA", "NA", None
|
|
401
|
+
model_name = _get_model_name(args, kwargs)
|
|
402
|
+
host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
|
|
403
|
+
fqm = "{}.{}".format(create.__module__, create.__qualname__)
|
|
404
|
+
context = self.agent.get_transaction_context()
|
|
405
|
+
if context:
|
|
406
|
+
context.entry_point_fqm = fqm
|
|
407
|
+
context.host_str = host_name_str
|
|
408
|
+
start_time = get_current_timestamp_in_us()
|
|
409
|
+
if self.agent.logger.isEnabledFor(logging.INFO):
|
|
410
|
+
self.agent.logger.info(
|
|
411
|
+
"openai async llm db call begin bt={} start_time={}".format(bt, start_time)
|
|
412
|
+
)
|
|
413
|
+
exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
|
|
416
|
+
if isinstance(e, NDHavocException):
|
|
417
|
+
raise e
|
|
418
|
+
self.agent.logger.exception("openai async llm db call begin: {}".format(e))
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
result = await create(*args, **kwargs)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
self.agent.logger.exception("openai async llm create error: {}".format(e))
|
|
424
|
+
try:
|
|
425
|
+
_record_call_error(self, bt, exit_call, start_time, fqm, context, username)
|
|
426
|
+
except Exception as inner:
|
|
427
|
+
self.agent.logger.exception("openai async llm db call end (error): {}".format(inner))
|
|
428
|
+
raise
|
|
429
|
+
|
|
430
|
+
# Async streaming path (Gap 3)
|
|
431
|
+
if kwargs.get("stream", False):
|
|
432
|
+
return AsyncStreamWrapper(
|
|
433
|
+
result,
|
|
434
|
+
_make_stream_on_finish(self, bt, exit_call, start_time, fqm,
|
|
435
|
+
context, username, args, kwargs)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Non-streaming path
|
|
439
|
+
try:
|
|
440
|
+
model_name = getattr(result, "model", None) or getattr(result, "model_name", None) or "default"
|
|
441
|
+
output = _build_chat_output(result)
|
|
442
|
+
usage = getattr(result, "usage", None)
|
|
443
|
+
_record_call_success(
|
|
444
|
+
self, bt, exit_call, start_time, fqm, context,
|
|
445
|
+
username, args, kwargs, model_name, output, usage
|
|
446
|
+
)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
self.agent.logger.exception("openai async llm db call end: {}".format(e))
|
|
449
|
+
|
|
450
|
+
return result
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Legacy (non-chat) Completions — Sync (Gap 6)
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
def _build_legacy_output(result):
|
|
458
|
+
"""Extract output from legacy Completion choices (result.choices[n].text)."""
|
|
459
|
+
choices = getattr(result, "choices", None) or []
|
|
460
|
+
outputs = [
|
|
461
|
+
{
|
|
462
|
+
"text": getattr(c, "text", None),
|
|
463
|
+
"finish_reason": getattr(c, "finish_reason", None),
|
|
464
|
+
"completion_id": getattr(result, "id", None),
|
|
465
|
+
}
|
|
466
|
+
for c in choices
|
|
467
|
+
]
|
|
468
|
+
if len(outputs) == 1:
|
|
469
|
+
return outputs[0]
|
|
470
|
+
return outputs if outputs else None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class OpenAILegacyCompletionsInterceptor(ExitCallInterceptor):
|
|
474
|
+
def wrap_create(self, create, *args, **kwargs):
|
|
475
|
+
start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
|
|
476
|
+
try:
|
|
477
|
+
bt = self.bt
|
|
478
|
+
username = "NA"
|
|
479
|
+
host, port, query, query_params = "NA", "NA", "NA", None
|
|
480
|
+
model_name = _get_model_name(args, kwargs)
|
|
481
|
+
host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
|
|
482
|
+
fqm = "{}.{}".format(create.__module__, create.__qualname__)
|
|
483
|
+
context = self.agent.get_transaction_context()
|
|
484
|
+
if context:
|
|
485
|
+
context.entry_point_fqm = fqm
|
|
486
|
+
context.host_str = host_name_str
|
|
487
|
+
start_time = get_current_timestamp_in_us()
|
|
488
|
+
exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
|
|
489
|
+
except Exception as e:
|
|
490
|
+
from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
|
|
491
|
+
if isinstance(e, NDHavocException):
|
|
492
|
+
raise e
|
|
493
|
+
self.agent.logger.exception("openai legacy completions db call begin: {}".format(e))
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
result = create(*args, **kwargs)
|
|
497
|
+
except Exception as e:
|
|
498
|
+
self.agent.logger.exception("openai legacy completions create error: {}".format(e))
|
|
499
|
+
try:
|
|
500
|
+
_record_call_error(self, bt, exit_call, start_time, fqm, context, username,
|
|
501
|
+
label="OpenAI Legacy Completions")
|
|
502
|
+
except Exception as inner:
|
|
503
|
+
self.agent.logger.exception("openai legacy completions db call end (error): {}".format(inner))
|
|
504
|
+
raise
|
|
505
|
+
|
|
506
|
+
if kwargs.get("stream", False):
|
|
507
|
+
return SyncStreamWrapper(
|
|
508
|
+
result,
|
|
509
|
+
_make_stream_on_finish(self, bt, exit_call, start_time, fqm,
|
|
510
|
+
context, username, args, kwargs)
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
model_name = getattr(result, "model", None) or "default"
|
|
515
|
+
output = _build_legacy_output(result)
|
|
516
|
+
usage = getattr(result, "usage", None)
|
|
517
|
+
_record_call_success(
|
|
518
|
+
self, bt, exit_call, start_time, fqm, context,
|
|
519
|
+
username, args, kwargs, model_name, output, usage,
|
|
520
|
+
label="OpenAI Legacy Completions"
|
|
521
|
+
)
|
|
522
|
+
except Exception as e:
|
|
523
|
+
self.agent.logger.exception("openai legacy completions db call end: {}".format(e))
|
|
524
|
+
|
|
525
|
+
return result
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# ---------------------------------------------------------------------------
|
|
529
|
+
# Legacy Completions — Async (Gap 6)
|
|
530
|
+
# ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
class AsyncOpenAILegacyCompletionsInterceptor(ExitCallInterceptor):
|
|
533
|
+
async def wrap_create(self, create, *args, **kwargs):
|
|
534
|
+
start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
|
|
535
|
+
try:
|
|
536
|
+
bt = self.bt
|
|
537
|
+
username = "NA"
|
|
538
|
+
host, port, query, query_params = "NA", "NA", "NA", None
|
|
539
|
+
model_name = _get_model_name(args, kwargs)
|
|
540
|
+
host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
|
|
541
|
+
fqm = "{}.{}".format(create.__module__, create.__qualname__)
|
|
542
|
+
context = self.agent.get_transaction_context()
|
|
543
|
+
if context:
|
|
544
|
+
context.entry_point_fqm = fqm
|
|
545
|
+
context.host_str = host_name_str
|
|
546
|
+
start_time = get_current_timestamp_in_us()
|
|
547
|
+
exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
|
|
548
|
+
except Exception as e:
|
|
549
|
+
from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
|
|
550
|
+
if isinstance(e, NDHavocException):
|
|
551
|
+
raise e
|
|
552
|
+
self.agent.logger.exception("openai async legacy completions db call begin: {}".format(e))
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
result = await create(*args, **kwargs)
|
|
556
|
+
except Exception as e:
|
|
557
|
+
self.agent.logger.exception("openai async legacy completions create error: {}".format(e))
|
|
558
|
+
try:
|
|
559
|
+
_record_call_error(self, bt, exit_call, start_time, fqm, context, username,
|
|
560
|
+
label="OpenAI Legacy Completions")
|
|
561
|
+
except Exception as inner:
|
|
562
|
+
self.agent.logger.exception("openai async legacy completions db call end (error): {}".format(inner))
|
|
563
|
+
raise
|
|
564
|
+
|
|
565
|
+
if kwargs.get("stream", False):
|
|
566
|
+
return AsyncStreamWrapper(
|
|
567
|
+
result,
|
|
568
|
+
_make_stream_on_finish(self, bt, exit_call, start_time, fqm,
|
|
569
|
+
context, username, args, kwargs)
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
try:
|
|
573
|
+
model_name = getattr(result, "model", None) or "default"
|
|
574
|
+
output = _build_legacy_output(result)
|
|
575
|
+
usage = getattr(result, "usage", None)
|
|
576
|
+
_record_call_success(
|
|
577
|
+
self, bt, exit_call, start_time, fqm, context,
|
|
578
|
+
username, args, kwargs, model_name, output, usage,
|
|
579
|
+
label="OpenAI Legacy Completions"
|
|
580
|
+
)
|
|
581
|
+
except Exception as e:
|
|
582
|
+
self.agent.logger.exception("openai async legacy completions db call end: {}".format(e))
|
|
583
|
+
|
|
584
|
+
return result
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
# ---------------------------------------------------------------------------
|
|
588
|
+
# Embeddings — Sync + Async (Gap 7)
|
|
589
|
+
# ---------------------------------------------------------------------------
|
|
590
|
+
|
|
591
|
+
def _build_embedding_output(result):
|
|
592
|
+
"""Build output list from EmbeddingResponse.data."""
|
|
593
|
+
data = getattr(result, "data", None) or []
|
|
594
|
+
vectors = [
|
|
595
|
+
{
|
|
596
|
+
"index": getattr(d, "index", i),
|
|
597
|
+
"embedding_length": len(getattr(d, "embedding", None) or []),
|
|
598
|
+
"embedding_sample": (getattr(d, "embedding", None) or [])[:5],
|
|
599
|
+
}
|
|
600
|
+
for i, d in enumerate(data)
|
|
601
|
+
]
|
|
602
|
+
return vectors[0] if len(vectors) == 1 else (vectors or None)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _record_embedding_span(interceptor, bt, exit_call, start_time, fqm,
|
|
606
|
+
context, username, kwargs, model_name, output, usage):
|
|
607
|
+
"""db_call_end + EmbeddingSpan."""
|
|
608
|
+
end_time = get_current_timestamp_in_us()
|
|
609
|
+
duration = get_validated_duration(start_time, end_time, "OpenAI Embeddings")
|
|
610
|
+
host_name_str = build_host_name_str("NA", "NA", "NA", model_name, username)
|
|
611
|
+
if context:
|
|
612
|
+
context.host_str = host_name_str
|
|
613
|
+
context.status_code = 200
|
|
614
|
+
context.db_callout_time = 0
|
|
615
|
+
interceptor.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
|
|
616
|
+
|
|
617
|
+
if not interceptor.agent.nf_enabled:
|
|
618
|
+
return
|
|
619
|
+
test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp = _split_fp(context)
|
|
620
|
+
meta = {
|
|
621
|
+
"span_kind": "embedding",
|
|
622
|
+
"input": kwargs.get("input") or kwargs.get("inputs"),
|
|
623
|
+
"output": output,
|
|
624
|
+
"model_name": model_name,
|
|
625
|
+
"model_provider": "OpenAI",
|
|
626
|
+
"metadata": kwargs.get("metadata", None),
|
|
627
|
+
}
|
|
628
|
+
embedding_span = EmbeddingSpan(
|
|
629
|
+
fqm, start_time, duration, "ok", meta,
|
|
630
|
+
test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp
|
|
631
|
+
)
|
|
632
|
+
# Attach token metrics (EmbeddingSpan inherits self.metrics = {} from BaseSpan)
|
|
633
|
+
embedding_span.metrics = _safe_usage_metrics(usage)
|
|
634
|
+
if interceptor.agent.logger.isEnabledFor(logging.INFO):
|
|
635
|
+
interceptor.agent.logger.info(
|
|
636
|
+
"openai embeddings sending span output={}".format(output)
|
|
637
|
+
)
|
|
638
|
+
interceptor.agent.dump_log_message(bt, embedding_span.create_span_json())
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class OpenAIEmbeddingsInterceptor(ExitCallInterceptor):
|
|
642
|
+
def wrap_create(self, create, *args, **kwargs):
|
|
643
|
+
start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
|
|
644
|
+
try:
|
|
645
|
+
bt = self.bt
|
|
646
|
+
username = "NA"
|
|
647
|
+
host, port, query, query_params = "NA", "NA", "NA", None
|
|
648
|
+
model_name = _get_model_name(args, kwargs)
|
|
649
|
+
host_name_str = build_host_name_str(host, port, "Embedding", "OpenAI_" + model_name, username)
|
|
650
|
+
fqm = "{}.{}".format(create.__module__, create.__qualname__)
|
|
651
|
+
context = self.agent.get_transaction_context()
|
|
652
|
+
if context:
|
|
653
|
+
context.entry_point_fqm = fqm
|
|
654
|
+
context.host_str = host_name_str
|
|
655
|
+
start_time = get_current_timestamp_in_us()
|
|
656
|
+
exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
|
|
657
|
+
except Exception as e:
|
|
658
|
+
from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
|
|
659
|
+
if isinstance(e, NDHavocException):
|
|
660
|
+
raise e
|
|
661
|
+
self.agent.logger.exception("openai embeddings db call begin: {}".format(e))
|
|
662
|
+
|
|
663
|
+
try:
|
|
664
|
+
result = create(*args, **kwargs)
|
|
665
|
+
except Exception as e:
|
|
666
|
+
self.agent.logger.exception("openai embeddings create error: {}".format(e))
|
|
667
|
+
try:
|
|
668
|
+
end_time = get_current_timestamp_in_us()
|
|
669
|
+
duration = get_validated_duration(start_time, end_time, "OpenAI Embeddings")
|
|
670
|
+
host_name_str = build_host_name_str("NA", "NA", "Embedding", "OpenAI_default", username)
|
|
671
|
+
if context:
|
|
672
|
+
context.host_str = host_name_str
|
|
673
|
+
context.status_code = 500
|
|
674
|
+
self.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
|
|
675
|
+
except Exception as inner:
|
|
676
|
+
self.agent.logger.exception("openai embeddings db call end (error): {}".format(inner))
|
|
677
|
+
raise
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
model_name = getattr(result, "model", None) or "default"
|
|
681
|
+
output = _build_embedding_output(result)
|
|
682
|
+
usage = getattr(result, "usage", None)
|
|
683
|
+
_record_embedding_span(
|
|
684
|
+
self, bt, exit_call, start_time, fqm, context,
|
|
685
|
+
username, kwargs, model_name, output, usage
|
|
686
|
+
)
|
|
687
|
+
except Exception as e:
|
|
688
|
+
self.agent.logger.exception("openai embeddings db call end: {}".format(e))
|
|
689
|
+
|
|
690
|
+
return result
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class AsyncOpenAIEmbeddingsInterceptor(ExitCallInterceptor):
|
|
694
|
+
async def wrap_create(self, create, *args, **kwargs):
|
|
695
|
+
start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
|
|
696
|
+
try:
|
|
697
|
+
bt = self.bt
|
|
698
|
+
username = "NA"
|
|
699
|
+
host, port, query, query_params = "NA", "NA", "NA", None
|
|
700
|
+
model_name = _get_model_name(args, kwargs)
|
|
701
|
+
host_name_str = build_host_name_str(host, port, "Embedding", "OpenAI_" + model_name, username)
|
|
702
|
+
fqm = "{}.{}".format(create.__module__, create.__qualname__)
|
|
703
|
+
context = self.agent.get_transaction_context()
|
|
704
|
+
if context:
|
|
705
|
+
context.entry_point_fqm = fqm
|
|
706
|
+
context.host_str = host_name_str
|
|
707
|
+
start_time = get_current_timestamp_in_us()
|
|
708
|
+
exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
|
|
709
|
+
except Exception as e:
|
|
710
|
+
from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
|
|
711
|
+
if isinstance(e, NDHavocException):
|
|
712
|
+
raise e
|
|
713
|
+
self.agent.logger.exception("openai async embeddings db call begin: {}".format(e))
|
|
714
|
+
|
|
715
|
+
try:
|
|
716
|
+
result = await create(*args, **kwargs)
|
|
717
|
+
except Exception as e:
|
|
718
|
+
self.agent.logger.exception("openai async embeddings create error: {}".format(e))
|
|
719
|
+
try:
|
|
720
|
+
end_time = get_current_timestamp_in_us()
|
|
721
|
+
duration = get_validated_duration(start_time, end_time, "OpenAI Embeddings")
|
|
722
|
+
host_name_str = build_host_name_str("NA", "NA", "Embedding", "OpenAI_default", username)
|
|
723
|
+
if context:
|
|
724
|
+
context.host_str = host_name_str
|
|
725
|
+
context.status_code = 500
|
|
726
|
+
self.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
|
|
727
|
+
except Exception as inner:
|
|
728
|
+
self.agent.logger.exception("openai async embeddings db call end (error): {}".format(inner))
|
|
729
|
+
raise
|
|
730
|
+
|
|
731
|
+
try:
|
|
732
|
+
model_name = getattr(result, "model", None) or "default"
|
|
733
|
+
output = _build_embedding_output(result)
|
|
734
|
+
usage = getattr(result, "usage", None)
|
|
735
|
+
_record_embedding_span(
|
|
736
|
+
self, bt, exit_call, start_time, fqm, context,
|
|
737
|
+
username, kwargs, model_name, output, usage
|
|
738
|
+
)
|
|
739
|
+
except Exception as e:
|
|
740
|
+
self.agent.logger.exception("openai async embeddings db call end: {}".format(e))
|
|
741
|
+
|
|
742
|
+
return result
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
# ---------------------------------------------------------------------------
|
|
746
|
+
# Registration
|
|
747
|
+
# ---------------------------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
def intercept_openai(agent, mod):
|
|
750
|
+
"""
|
|
751
|
+
Patch all relevant OpenAI SDK entry points.
|
|
752
|
+
Each registration is wrapped independently so a missing class on an older
|
|
753
|
+
SDK version does not prevent the others from being instrumented.
|
|
754
|
+
"""
|
|
755
|
+
agent.logger.warning("Instrument module: openai{}".format(", mod: {}".format(mod) if mod else ""))
|
|
756
|
+
# 1. Sync chat completions (fixed: null-safety, positional messages, streaming, all choices)
|
|
757
|
+
try:
|
|
758
|
+
OpenAIInterceptor(agent, mod.resources.chat.completions.Completions) \
|
|
759
|
+
.attach('create', patched_method_name='wrap_create')
|
|
760
|
+
except Exception as e:
|
|
761
|
+
agent.logger.exception("openai sync chat completions intercept failed: {}".format(e))
|
|
762
|
+
|
|
763
|
+
# 2. Async chat completions (Gap 1 + Gap 3)
|
|
764
|
+
try:
|
|
765
|
+
AsyncOpenAIInterceptor(agent, mod.resources.chat.completions.AsyncCompletions) \
|
|
766
|
+
.attach('create', patched_method_name='wrap_create')
|
|
767
|
+
except Exception as e:
|
|
768
|
+
agent.logger.exception("openai async chat completions intercept failed: {}".format(e))
|
|
769
|
+
|
|
770
|
+
# 3. Legacy completions — sync (Gap 6)
|
|
771
|
+
try:
|
|
772
|
+
OpenAILegacyCompletionsInterceptor(agent, mod.resources.completions.Completions) \
|
|
773
|
+
.attach('create', patched_method_name='wrap_create')
|
|
774
|
+
except Exception as e:
|
|
775
|
+
agent.logger.exception("openai legacy sync completions intercept failed: {}".format(e))
|
|
776
|
+
|
|
777
|
+
# 4. Legacy completions — async (Gap 6)
|
|
778
|
+
try:
|
|
779
|
+
AsyncOpenAILegacyCompletionsInterceptor(agent, mod.resources.completions.AsyncCompletions) \
|
|
780
|
+
.attach('create', patched_method_name='wrap_create')
|
|
781
|
+
except Exception as e:
|
|
782
|
+
agent.logger.exception("openai legacy async completions intercept failed: {}".format(e))
|
|
783
|
+
|
|
784
|
+
# 5. Embeddings — sync (Gap 7)
|
|
785
|
+
try:
|
|
786
|
+
OpenAIEmbeddingsInterceptor(agent, mod.resources.embeddings.Embeddings) \
|
|
787
|
+
.attach('create', patched_method_name='wrap_create')
|
|
788
|
+
except Exception as e:
|
|
789
|
+
agent.logger.exception("openai sync embeddings intercept failed: {}".format(e))
|
|
790
|
+
return OpenAIInterceptor(agent, mod.resources.chat.completions.Completions).attach('create', patched_method_name='wrap_create')
|
|
791
|
+
|
|
792
|
+
# 6. Embeddings — async (Gap 7)
|
|
793
|
+
try:
|
|
794
|
+
AsyncOpenAIEmbeddingsInterceptor(agent, mod.resources.embeddings.AsyncEmbeddings) \
|
|
795
|
+
.attach('create', patched_method_name='wrap_create')
|
|
796
|
+
except Exception as e:
|
|
797
|
+
agent.logger.exception("openai async embeddings intercept failed: {}".format(e))
|