whatap-python 2.1.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.
Files changed (227) hide show
  1. whatap/LICENSE +0 -0
  2. whatap/README.rst +49 -0
  3. whatap/__init__.py +923 -0
  4. whatap/__main__.py +4 -0
  5. whatap/agent/darwin/amd64/whatap_python +0 -0
  6. whatap/agent/darwin/arm64/whatap_python +0 -0
  7. whatap/agent/linux/amd64/whatap_python +0 -0
  8. whatap/agent/linux/arm64/whatap_python +0 -0
  9. whatap/agent/windows/whatap_python.exe +0 -0
  10. whatap/bootstrap/__init__.py +0 -0
  11. whatap/bootstrap/sitecustomize.py +19 -0
  12. whatap/build.py +4 -0
  13. whatap/conf/__init__.py +0 -0
  14. whatap/conf/configuration.py +280 -0
  15. whatap/conf/configure.py +105 -0
  16. whatap/conf/license.py +49 -0
  17. whatap/control/__init__.py +0 -0
  18. whatap/counter/__init__.py +14 -0
  19. whatap/counter/counter_manager.py +45 -0
  20. whatap/counter/tasks/__init__.py +3 -0
  21. whatap/counter/tasks/base_task.py +26 -0
  22. whatap/counter/tasks/llm_evaluator_task.py +501 -0
  23. whatap/counter/tasks/llm_log_sink_task.py +309 -0
  24. whatap/counter/tasks/llm_stat_task.py +78 -0
  25. whatap/counter/tasks/openfiledescriptor.py +67 -0
  26. whatap/io/__init__.py +1 -0
  27. whatap/io/data_inputx.py +161 -0
  28. whatap/io/data_outputx.py +262 -0
  29. whatap/llm/__init__.py +17 -0
  30. whatap/llm/definitions.py +43 -0
  31. whatap/llm/evaluators/__init__.py +136 -0
  32. whatap/llm/evaluators/base.py +114 -0
  33. whatap/llm/evaluators/builtins/__init__.py +91 -0
  34. whatap/llm/evaluators/builtins/answer_relevance.py +46 -0
  35. whatap/llm/evaluators/builtins/combined_judge.py +271 -0
  36. whatap/llm/evaluators/builtins/factuality.py +71 -0
  37. whatap/llm/evaluators/builtins/hallucination.py +97 -0
  38. whatap/llm/evaluators/builtins/llm_judge.py +516 -0
  39. whatap/llm/evaluators/builtins/pii_leak.py +214 -0
  40. whatap/llm/evaluators/builtins/prompt_injection.py +71 -0
  41. whatap/llm/evaluators/builtins/toxicity.py +53 -0
  42. whatap/llm/evaluators/builtins/url_scan.py +194 -0
  43. whatap/llm/evaluators/registry.py +192 -0
  44. whatap/llm/evaluators/sampler.py +83 -0
  45. whatap/llm/evaluators/scope.py +334 -0
  46. whatap/llm/features.py +66 -0
  47. whatap/llm/log_sink_packs/__init__.py +9 -0
  48. whatap/llm/log_sink_packs/llm_input_message.py +16 -0
  49. whatap/llm/log_sink_packs/llm_log_sink_pack.py +72 -0
  50. whatap/llm/log_sink_packs/llm_output_message.py +19 -0
  51. whatap/llm/log_sink_packs/llm_step_eval_status.py +94 -0
  52. whatap/llm/log_sink_packs/llm_step_status.py +118 -0
  53. whatap/llm/log_sink_packs/llm_system_message.py +16 -0
  54. whatap/llm/log_sink_packs/llm_tool_calls.py +44 -0
  55. whatap/llm/log_sink_packs/llm_tool_results.py +16 -0
  56. whatap/llm/log_sink_packs/llm_tx_status.py +108 -0
  57. whatap/llm/pricing.py +236 -0
  58. whatap/llm/prompt_meta.py +288 -0
  59. whatap/llm/providers/__init__.py +0 -0
  60. whatap/llm/providers/anthropic/__init__.py +37 -0
  61. whatap/llm/providers/anthropic/messages/__init__.py +0 -0
  62. whatap/llm/providers/anthropic/messages/messages.py +70 -0
  63. whatap/llm/providers/anthropic/messages/messages_context.py +76 -0
  64. whatap/llm/providers/anthropic/messages/messages_extractor.py +126 -0
  65. whatap/llm/providers/interceptor.py +182 -0
  66. whatap/llm/providers/openai/__init__.py +133 -0
  67. whatap/llm/providers/openai/chat/__init__.py +0 -0
  68. whatap/llm/providers/openai/chat/chat.py +82 -0
  69. whatap/llm/providers/openai/chat/chat_context.py +78 -0
  70. whatap/llm/providers/openai/chat/chat_extractor.py +127 -0
  71. whatap/llm/providers/openai/completions/__init__.py +0 -0
  72. whatap/llm/providers/openai/completions/completions.py +70 -0
  73. whatap/llm/providers/openai/completions/completions_context.py +31 -0
  74. whatap/llm/providers/openai/completions/completions_extractor.py +61 -0
  75. whatap/llm/providers/openai/content_parser.py +41 -0
  76. whatap/llm/providers/openai/embeddings/__init__.py +0 -0
  77. whatap/llm/providers/openai/embeddings/embeddings.py +59 -0
  78. whatap/llm/providers/openai/embeddings/embeddings_context.py +25 -0
  79. whatap/llm/providers/openai/embeddings/embeddings_extractor.py +26 -0
  80. whatap/llm/providers/openai/responses/__init__.py +0 -0
  81. whatap/llm/providers/openai/responses/responses.py +70 -0
  82. whatap/llm/providers/openai/responses/responses_context.py +88 -0
  83. whatap/llm/providers/openai/responses/responses_extractor.py +126 -0
  84. whatap/llm/providers/stream_accumulator.py +73 -0
  85. whatap/llm/stats/__init__.py +35 -0
  86. whatap/llm/stats/active_stat.py +86 -0
  87. whatap/llm/stats/answer_relevance_eval_stat.py +10 -0
  88. whatap/llm/stats/api_status_stat.py +35 -0
  89. whatap/llm/stats/base_stat.py +107 -0
  90. whatap/llm/stats/combined_judge_eval_stat.py +11 -0
  91. whatap/llm/stats/error_stat.py +59 -0
  92. whatap/llm/stats/eval_stat.py +225 -0
  93. whatap/llm/stats/factuality_eval_stat.py +10 -0
  94. whatap/llm/stats/feature_stat.py +104 -0
  95. whatap/llm/stats/finish_stat.py +105 -0
  96. whatap/llm/stats/hallucination_eval_stat.py +10 -0
  97. whatap/llm/stats/meter.py +18 -0
  98. whatap/llm/stats/perf_stat.py +117 -0
  99. whatap/llm/stats/pii_leak_eval_stat.py +12 -0
  100. whatap/llm/stats/prompt_injection_eval_stat.py +10 -0
  101. whatap/llm/stats/token_usage_stat.py +133 -0
  102. whatap/llm/stats/toxicity_eval_stat.py +10 -0
  103. whatap/llm/stats/url_scan_eval_stat.py +12 -0
  104. whatap/net/__init__.py +0 -0
  105. whatap/net/async_sender.py +107 -0
  106. whatap/net/packet_enum.py +44 -0
  107. whatap/net/packet_type_enum.py +31 -0
  108. whatap/net/param_def.py +69 -0
  109. whatap/net/stackhelper.py +87 -0
  110. whatap/net/udp_session.py +394 -0
  111. whatap/net/udp_thread.py +54 -0
  112. whatap/pack/__init__.py +0 -0
  113. whatap/pack/logSinkPack.py +77 -0
  114. whatap/pack/pack.py +34 -0
  115. whatap/pack/pack_enum.py +41 -0
  116. whatap/pack/tagCountPack.py +61 -0
  117. whatap/scripts/__init__.py +208 -0
  118. whatap/trace/__init__.py +12 -0
  119. whatap/trace/mod/__init__.py +0 -0
  120. whatap/trace/mod/amqp/__init__.py +0 -0
  121. whatap/trace/mod/amqp/kombu.py +122 -0
  122. whatap/trace/mod/amqp/pika.py +62 -0
  123. whatap/trace/mod/application/__init__.py +0 -0
  124. whatap/trace/mod/application/bottle.py +34 -0
  125. whatap/trace/mod/application/celery.py +81 -0
  126. whatap/trace/mod/application/cherrypy.py +30 -0
  127. whatap/trace/mod/application/django.py +287 -0
  128. whatap/trace/mod/application/django_asgi.py +266 -0
  129. whatap/trace/mod/application/django_py3.py +251 -0
  130. whatap/trace/mod/application/fastapi/__init__.py +31 -0
  131. whatap/trace/mod/application/fastapi/endpoint.py +73 -0
  132. whatap/trace/mod/application/fastapi/exception_log.py +63 -0
  133. whatap/trace/mod/application/fastapi/instrumentation.py +204 -0
  134. whatap/trace/mod/application/fastapi/scope.py +115 -0
  135. whatap/trace/mod/application/fastapi/transaction.py +67 -0
  136. whatap/trace/mod/application/flask.py +52 -0
  137. whatap/trace/mod/application/frappe.py +224 -0
  138. whatap/trace/mod/application/graphql.py +170 -0
  139. whatap/trace/mod/application/nameko.py +39 -0
  140. whatap/trace/mod/application/odoo.py +63 -0
  141. whatap/trace/mod/application/starlette.py +126 -0
  142. whatap/trace/mod/application/tornado.py +163 -0
  143. whatap/trace/mod/application/wsgi.py +195 -0
  144. whatap/trace/mod/database/__init__.py +0 -0
  145. whatap/trace/mod/database/cxoracle.py +49 -0
  146. whatap/trace/mod/database/mongo.py +169 -0
  147. whatap/trace/mod/database/mysql.py +80 -0
  148. whatap/trace/mod/database/neo4j.py +90 -0
  149. whatap/trace/mod/database/psycopg2.py +45 -0
  150. whatap/trace/mod/database/psycopg3.py +359 -0
  151. whatap/trace/mod/database/redis.py +122 -0
  152. whatap/trace/mod/database/sqlalchemy.py +213 -0
  153. whatap/trace/mod/database/sqlite3.py +130 -0
  154. whatap/trace/mod/database/util.py +630 -0
  155. whatap/trace/mod/email/__init__.py +0 -0
  156. whatap/trace/mod/email/smtp.py +78 -0
  157. whatap/trace/mod/httpc/__init__.py +0 -0
  158. whatap/trace/mod/httpc/django.py +31 -0
  159. whatap/trace/mod/httpc/httplib.py +70 -0
  160. whatap/trace/mod/httpc/httpx.py +62 -0
  161. whatap/trace/mod/httpc/requests.py +20 -0
  162. whatap/trace/mod/httpc/urllib3.py +27 -0
  163. whatap/trace/mod/httpc/util.py +388 -0
  164. whatap/trace/mod/logging.py +161 -0
  165. whatap/trace/mod/plugin.py +84 -0
  166. whatap/trace/mod/standalone/__init__.py +0 -0
  167. whatap/trace/mod/standalone/multiple.py +293 -0
  168. whatap/trace/mod/standalone/single.py +135 -0
  169. whatap/trace/simple_trace_context.py +18 -0
  170. whatap/trace/trace_context.py +212 -0
  171. whatap/trace/trace_context_manager.py +244 -0
  172. whatap/trace/trace_error.py +84 -0
  173. whatap/trace/trace_handler.py +89 -0
  174. whatap/trace/trace_import.py +91 -0
  175. whatap/trace/trace_module_definition.py +156 -0
  176. whatap/util/__init__.py +0 -0
  177. whatap/util/bit_util.py +49 -0
  178. whatap/util/cardinality/__init__.py +0 -0
  179. whatap/util/cardinality/hyperloglog.py +84 -0
  180. whatap/util/cardinality/murmurhash.py +20 -0
  181. whatap/util/cardinality/registerset.py +60 -0
  182. whatap/util/compare_util.py +19 -0
  183. whatap/util/date_util.py +55 -0
  184. whatap/util/debug_util.py +73 -0
  185. whatap/util/escape_literal_sql.py +233 -0
  186. whatap/util/frame_util.py +20 -0
  187. whatap/util/hash_util.py +103 -0
  188. whatap/util/hexa32.py +66 -0
  189. whatap/util/int_set.py +199 -0
  190. whatap/util/ip_util.py +63 -0
  191. whatap/util/keygen.py +11 -0
  192. whatap/util/linked_list.py +113 -0
  193. whatap/util/linked_map.py +359 -0
  194. whatap/util/metering_util.py +103 -0
  195. whatap/util/request_double_queue.py +68 -0
  196. whatap/util/request_queue.py +60 -0
  197. whatap/util/string_util.py +20 -0
  198. whatap/util/throttle_util.py +99 -0
  199. whatap/util/userid_util.py +134 -0
  200. whatap/value/__init__.py +1 -0
  201. whatap/value/blob_value.py +38 -0
  202. whatap/value/boolean_value.py +33 -0
  203. whatap/value/decimal_value.py +36 -0
  204. whatap/value/double_summary.py +86 -0
  205. whatap/value/double_value.py +33 -0
  206. whatap/value/float_array.py +42 -0
  207. whatap/value/float_value.py +34 -0
  208. whatap/value/int_array.py +42 -0
  209. whatap/value/ip4_value.py +50 -0
  210. whatap/value/list_value.py +105 -0
  211. whatap/value/long_array.py +44 -0
  212. whatap/value/long_summary.py +83 -0
  213. whatap/value/map_value.py +154 -0
  214. whatap/value/null_value.py +21 -0
  215. whatap/value/number_value.py +33 -0
  216. whatap/value/summary_value.py +39 -0
  217. whatap/value/text_array.py +58 -0
  218. whatap/value/text_hash_value.py +37 -0
  219. whatap/value/text_value.py +43 -0
  220. whatap/value/value.py +26 -0
  221. whatap/value/value_enum.py +80 -0
  222. whatap/whatap.conf +14 -0
  223. whatap_python-2.1.0.dist-info/METADATA +87 -0
  224. whatap_python-2.1.0.dist-info/RECORD +227 -0
  225. whatap_python-2.1.0.dist-info/WHEEL +5 -0
  226. whatap_python-2.1.0.dist-info/entry_points.txt +6 -0
  227. whatap_python-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,127 @@
1
+ """OpenAI Chat Completions 응답에서 토큰 사용량 및 완성 텍스트를 추출하는 모듈."""
2
+ import json
3
+
4
+ from whatap.llm.features import LlmFeature
5
+ from whatap.llm.providers.stream_accumulator import StreamAccumulator
6
+
7
+
8
+ def extract_usage(usage):
9
+ """usage 객체에서 토큰 사용량 상세 정보를 딕셔너리로 추출한다."""
10
+ input_t = getattr(usage, "prompt_tokens", 0) or 0
11
+ output_t = getattr(usage, "completion_tokens", 0) or 0
12
+ prompt_d = getattr(usage, "prompt_tokens_details", None)
13
+ comp_d = getattr(usage, "completion_tokens_details", None)
14
+ return {
15
+ "input_tokens": input_t,
16
+ "output_tokens": output_t,
17
+ "total_tokens_count": input_t + output_t,
18
+ "cached_tokens": getattr(prompt_d, "cached_tokens", 0) or 0,
19
+ "audio_input_tokens": getattr(prompt_d, "audio_tokens", 0) or 0,
20
+ "reasoning_tokens": getattr(comp_d, "reasoning_tokens", 0) or 0,
21
+ "audio_output_tokens": getattr(comp_d, "audio_tokens", 0) or 0,
22
+ "accepted_prediction_tokens": getattr(comp_d, "accepted_prediction_tokens", 0) or 0,
23
+ "rejected_prediction_tokens": getattr(comp_d, "rejected_prediction_tokens", 0) or 0,
24
+ }
25
+
26
+
27
+ def finalize(response, pack, features):
28
+ """비스트리밍 Chat 응답에서 완성 텍스트, 토큰, 도구 호출 정보를 팩에 기록한다."""
29
+ texts, reasonings = [], []
30
+ for choice in response.choices:
31
+ if not pack.finish_reason:
32
+ pack.finish_reason = getattr(choice, 'finish_reason', None)
33
+ texts.append(choice.message.content or "")
34
+ reasonings.append(
35
+ getattr(choice.message, "reasoning_content", "") or
36
+ getattr(choice.message, "reasoning", "") or "")
37
+ tc = getattr(choice.message, "tool_calls", None)
38
+ if tc and LlmFeature.TOOL_USE not in features:
39
+ features.append(LlmFeature.TOOL_USE)
40
+ try:
41
+ pack.tool_calls_text = json.dumps([t.model_dump() for t in tc], ensure_ascii=False)
42
+ except Exception:
43
+ pack.tool_calls_text = str(tc)
44
+
45
+ reasoning = "".join(reasonings)
46
+ if reasoning and LlmFeature.REASONING not in features:
47
+ features.append(LlmFeature.REASONING)
48
+
49
+ pack.features = ",".join(features)
50
+ pack.set_tokens(extract_usage(getattr(response, "usage", None)))
51
+ pack.completion_text = "".join(texts)
52
+ if reasoning:
53
+ pack.reasoning_text = reasoning
54
+
55
+
56
+ class ChatStream(StreamAccumulator):
57
+ """OpenAI Chat 스트리밍 응답 청크를 누적하는 어큐뮬레이터."""
58
+
59
+ def __init__(self, pack, active_key):
60
+ super().__init__(pack, active_key)
61
+ self.text = ""
62
+ self.reasoning = ""
63
+ self.usage = {}
64
+ self.tool_chunks = {}
65
+ self.has_tool = False
66
+ self.finish_reason = None
67
+
68
+ def on_chunk(self, chunk):
69
+ usage = getattr(chunk, "usage", None)
70
+ if usage:
71
+ self.usage = extract_usage(usage)
72
+
73
+ choices = getattr(chunk, 'choices', None)
74
+ if not choices:
75
+ return
76
+
77
+ finish_reason = getattr(choices[0], 'finish_reason', None)
78
+ if finish_reason:
79
+ self.finish_reason = finish_reason
80
+
81
+ delta = choices[0].delta
82
+
83
+ content = getattr(delta, "content", None) or ""
84
+ if content:
85
+ self.on_first_token()
86
+ self.text += content
87
+
88
+ reasoning = (
89
+ getattr(delta, "reasoning_content", None) or
90
+ getattr(delta, "reasoning", None) or "")
91
+ if reasoning:
92
+ self.reasoning += reasoning
93
+
94
+ if getattr(delta, "tool_calls", None):
95
+ self.has_tool = True
96
+ for tc in delta.tool_calls:
97
+ idx = tc.index
98
+ if idx not in self.tool_chunks:
99
+ self.tool_chunks[idx] = {"id": "", "function": "", "arguments": ""}
100
+ if tc.id:
101
+ self.tool_chunks[idx]["id"] = tc.id
102
+ fn = getattr(tc, "function", None)
103
+ if fn:
104
+ if fn.name:
105
+ self.tool_chunks[idx]["function"] = fn.name
106
+ if fn.arguments:
107
+ self.tool_chunks[idx]["arguments"] += fn.arguments
108
+
109
+ def _apply(self):
110
+ pack = self.pack
111
+ pack.finish_reason = self.finish_reason
112
+ if self.has_tool:
113
+ parts = [f for f in pack.features.split(",") if f]
114
+ if LlmFeature.TOOL_USE not in parts:
115
+ parts.append(LlmFeature.TOOL_USE)
116
+ pack.features = ",".join(parts)
117
+ if self.reasoning:
118
+ parts = [f for f in pack.features.split(",") if f]
119
+ if LlmFeature.REASONING not in parts:
120
+ parts.append(LlmFeature.REASONING)
121
+ pack.features = ",".join(parts)
122
+ if self.tool_chunks:
123
+ pack.tool_calls_text = json.dumps(list(self.tool_chunks.values()), ensure_ascii=False)
124
+ pack.set_tokens(self.usage)
125
+ pack.completion_text = self.text
126
+ if self.reasoning:
127
+ pack.reasoning_text = self.reasoning
File without changes
@@ -0,0 +1,70 @@
1
+ """OpenAI Completions API 인터셉트 진입점."""
2
+ from openai import OpenAIError
3
+
4
+ from whatap.llm.providers.interceptor import (
5
+ before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
+ capture_client,
7
+ )
8
+ from whatap.llm.providers.stream_accumulator import sync_stream, async_stream
9
+ from whatap.llm.providers.openai.completions.completions_context import build_context
10
+ from whatap.llm.providers.openai.completions.completions_extractor import finalize, CompletionsStream
11
+
12
+
13
+ def intercept_completions(fn, *args, **kwargs):
14
+ """Completions API 동기 인터셉트."""
15
+ pack, ctx = build_context(kwargs)
16
+ capture_client(pack, ctx, args)
17
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
18
+
19
+ before_call(pack, active_key)
20
+ _stream_returned = False
21
+ try:
22
+ try:
23
+ response = fn(*args, **kwargs)
24
+ except Exception as err:
25
+ handle_error(pack, err, active_key, OpenAIError)
26
+ raise
27
+ finally:
28
+ if ctx:
29
+ ctx._llm_httpc_pending = False
30
+
31
+ after_call(pack, ctx)
32
+ if pack.stream:
33
+ _stream_returned = True
34
+ return sync_stream(response, CompletionsStream(pack, active_key))
35
+ finalize(response, pack)
36
+ finalize_non_streaming(pack, active_key)
37
+ return response
38
+ finally:
39
+ if not _stream_returned:
40
+ _ensure_end(pack, active_key)
41
+
42
+
43
+ async def intercept_completions_async(fn, *args, **kwargs):
44
+ """Completions API 비동기 인터셉트."""
45
+ pack, ctx = build_context(kwargs)
46
+ capture_client(pack, ctx, args)
47
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
48
+
49
+ before_call(pack, active_key)
50
+ _stream_returned = False
51
+ try:
52
+ try:
53
+ response = await fn(*args, **kwargs)
54
+ except Exception as err:
55
+ handle_error(pack, err, active_key, OpenAIError)
56
+ raise
57
+ finally:
58
+ if ctx:
59
+ ctx._llm_httpc_pending = False
60
+
61
+ after_call(pack, ctx)
62
+ if pack.stream:
63
+ _stream_returned = True
64
+ return async_stream(response, CompletionsStream(pack, active_key))
65
+ finalize(response, pack)
66
+ finalize_non_streaming(pack, active_key)
67
+ return response
68
+ finally:
69
+ if not _stream_returned:
70
+ _ensure_end(pack, active_key)
@@ -0,0 +1,31 @@
1
+ """OpenAI Completions API 요청 파싱."""
2
+ import json
3
+
4
+ from whatap.trace.trace_context_manager import TraceContextManager
5
+ from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
6
+
7
+
8
+ def build_context(kwargs):
9
+ """Completions kwargs에서 LlmStepStatus 팩을 생성한다."""
10
+ model = kwargs.get("model")
11
+ stream = kwargs.get("stream", False)
12
+ prompt = kwargs.get("prompt", "")
13
+
14
+ if isinstance(prompt, list):
15
+ prompt_text = json.dumps(prompt, ensure_ascii=False)
16
+ else:
17
+ prompt_text = str(prompt) if prompt else ""
18
+
19
+ pack = LlmStepStatus()
20
+ pack.model = model
21
+ pack.prompt_text = prompt_text
22
+ pack.stream = stream
23
+ if kwargs.get("temperature") is not None:
24
+ pack.temperature = kwargs["temperature"]
25
+
26
+ ctx = TraceContextManager.getLocalContext()
27
+ if ctx:
28
+ ctx._llm_httpc_pending = True
29
+ ctx._llm_model = model
30
+
31
+ return pack, ctx
@@ -0,0 +1,61 @@
1
+ """OpenAI Completions API 응답 추출."""
2
+ from whatap.llm.providers.stream_accumulator import StreamAccumulator
3
+
4
+
5
+ def extract_usage(usage):
6
+ """usage 객체에서 토큰 사용량을 추출한다."""
7
+ if not usage:
8
+ return {}
9
+ input_t = getattr(usage, "prompt_tokens", 0) or 0
10
+ output_t = getattr(usage, "completion_tokens", 0) or 0
11
+ return {
12
+ "input_tokens": input_t,
13
+ "output_tokens": output_t,
14
+ "total_tokens_count": input_t + output_t,
15
+ }
16
+
17
+
18
+ def finalize(response, pack):
19
+ """비스트리밍 Completions 응답에서 텍스트와 토큰을 팩에 기록한다."""
20
+ texts = []
21
+ for choice in response.choices:
22
+ if not pack.finish_reason:
23
+ pack.finish_reason = getattr(choice, 'finish_reason', None)
24
+ texts.append(getattr(choice, 'text', '') or '')
25
+
26
+ pack.set_tokens(extract_usage(getattr(response, "usage", None)))
27
+ pack.completion_text = "".join(texts)
28
+
29
+
30
+ class CompletionsStream(StreamAccumulator):
31
+ """OpenAI Completions 스트리밍 응답 청크를 누적하는 어큐뮬레이터."""
32
+
33
+ def __init__(self, pack, active_key):
34
+ super().__init__(pack, active_key)
35
+ self.text = ""
36
+ self.usage = {}
37
+ self.finish_reason = None
38
+
39
+ def on_chunk(self, chunk):
40
+ usage = getattr(chunk, "usage", None)
41
+ if usage:
42
+ self.usage = extract_usage(usage)
43
+
44
+ choices = getattr(chunk, 'choices', None)
45
+ if not choices:
46
+ return
47
+
48
+ finish_reason = getattr(choices[0], 'finish_reason', None)
49
+ if finish_reason:
50
+ self.finish_reason = finish_reason
51
+
52
+ text = getattr(choices[0], 'text', '') or ''
53
+ if text:
54
+ self.on_first_token()
55
+ self.text += text
56
+
57
+ def _apply(self):
58
+ pack = self.pack
59
+ pack.finish_reason = self.finish_reason
60
+ pack.set_tokens(self.usage)
61
+ pack.completion_text = self.text
@@ -0,0 +1,41 @@
1
+ """OpenAI 컨텐츠 블록 파싱.
2
+
3
+ OpenAI API의 content 필드는 문자열 또는 블록 리스트(text, image_url 등)로 올 수 있다.
4
+ 이 모듈은 다양한 content 형태를 텍스트로 변환한다.
5
+ """
6
+
7
+
8
+ def _block_to_dict(block):
9
+ """Pydantic 모델 등 비dict 블록을 dict로 변환."""
10
+ if isinstance(block, dict):
11
+ return block
12
+ if hasattr(block, 'model_dump'):
13
+ try:
14
+ return block.model_dump()
15
+ except Exception:
16
+ pass
17
+ if hasattr(block, '__dict__'):
18
+ return dict(vars(block))
19
+ return {"type": str(getattr(block, 'type', ''))}
20
+
21
+
22
+ def extract_content_text(content):
23
+ """content를 텍스트로 변환. 이미지/오디오 블록은 [IMAGE], [AUDIO]로 치환."""
24
+ if isinstance(content, str):
25
+ return content
26
+ if isinstance(content, list):
27
+ parts = []
28
+ for block in content:
29
+ if not isinstance(block, dict):
30
+ block = _block_to_dict(block)
31
+ block_type = block.get("type", "")
32
+ if block_type in ("text", "input_text"):
33
+ parts.append(block.get("text", ""))
34
+ elif block_type in ("image_url", "input_image"):
35
+ parts.append("[IMAGE]")
36
+ elif block_type == "input_audio":
37
+ parts.append("[AUDIO]")
38
+ elif block_type:
39
+ parts.append("[{}]".format(block_type.upper()))
40
+ return "".join(parts)
41
+ return str(content) if content else ""
File without changes
@@ -0,0 +1,59 @@
1
+ """OpenAI Embeddings API 호출을 인터셉트하는 모듈."""
2
+ from openai import OpenAIError
3
+
4
+ from whatap.llm.providers.interceptor import (
5
+ before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
+ capture_client,
7
+ )
8
+ from whatap.llm.providers.openai.embeddings.embeddings_context import build_context
9
+ from whatap.llm.providers.openai.embeddings.embeddings_extractor import finalize
10
+
11
+
12
+ def intercept_embeddings(fn, *args, **kwargs):
13
+ """OpenAI Embeddings 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
14
+ pack, ctx = build_context(kwargs)
15
+ capture_client(pack, ctx, args)
16
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
17
+
18
+ before_call(pack, active_key)
19
+ try:
20
+ try:
21
+ response = fn(*args, **kwargs)
22
+ except Exception as err:
23
+ handle_error(pack, err, active_key, OpenAIError)
24
+ raise
25
+ finally:
26
+ if ctx:
27
+ ctx._llm_httpc_pending = False
28
+
29
+ after_call(pack, ctx)
30
+ finalize(response, pack, kwargs)
31
+ finalize_non_streaming(pack, active_key)
32
+ return response
33
+ finally:
34
+ _ensure_end(pack, active_key)
35
+
36
+
37
+ async def intercept_embeddings_async(fn, *args, **kwargs):
38
+ """OpenAI Embeddings 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
39
+ pack, ctx = build_context(kwargs)
40
+ capture_client(pack, ctx, args)
41
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
42
+
43
+ before_call(pack, active_key)
44
+ try:
45
+ try:
46
+ response = await fn(*args, **kwargs)
47
+ except Exception as err:
48
+ handle_error(pack, err, active_key, OpenAIError)
49
+ raise
50
+ finally:
51
+ if ctx:
52
+ ctx._llm_httpc_pending = False
53
+
54
+ after_call(pack, ctx)
55
+ finalize(response, pack, kwargs)
56
+ finalize_non_streaming(pack, active_key)
57
+ return response
58
+ finally:
59
+ _ensure_end(pack, active_key)
@@ -0,0 +1,25 @@
1
+ """OpenAI Embeddings API 요청에서 컨텍스트를 구성하는 모듈."""
2
+ import json
3
+
4
+ from whatap.trace.trace_context_manager import TraceContextManager
5
+ from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
6
+
7
+
8
+ def build_context(kwargs):
9
+ """Embeddings kwargs로부터 LlmStepStatus 팩과 트레이스 컨텍스트를 구성한다."""
10
+ input_data = kwargs.get("input", "")
11
+ model = kwargs.get("model")
12
+
13
+ inputs = input_data if isinstance(input_data, list) else [input_data]
14
+
15
+ pack = LlmStepStatus()
16
+ pack.model = model
17
+ pack.prompt_text = json.dumps(inputs, ensure_ascii=False)
18
+ pack.stream = False
19
+
20
+ ctx = TraceContextManager.getLocalContext()
21
+ if ctx:
22
+ ctx._llm_httpc_pending = True
23
+ ctx._llm_model = model
24
+
25
+ return pack, ctx
@@ -0,0 +1,26 @@
1
+ """OpenAI Embeddings 응답에서 토큰 사용량과 임베딩 정보를 추출하는 모듈."""
2
+ from whatap import logging
3
+
4
+
5
+ def finalize(response, pack, kwargs):
6
+ """Embeddings 응답에서 토큰 수, 임베딩 개수, 차원 수를 팩에 기록한다."""
7
+ usage = getattr(response, "usage", None)
8
+ input_tokens = getattr(usage, "prompt_tokens", 0) or 0
9
+
10
+ embedding_count = 0
11
+ dimensions = 0
12
+ try:
13
+ data = getattr(response, "data", [])
14
+ embedding_count = len(data)
15
+ if data and kwargs.get("encoding_format") != "base64":
16
+ dimensions = len(data[0].embedding)
17
+ except Exception as e:
18
+ logging.warning('[LLM] embeddings data extraction failed: %s' % e, extra={'id': 'LLM010'})
19
+
20
+ pack.set_tokens({
21
+ "input_tokens": input_tokens,
22
+ "output_tokens": 0,
23
+ "total_tokens_count": getattr(usage, "total_tokens", 0) or 0,
24
+ "embedding_count": embedding_count,
25
+ "dimensions": dimensions,
26
+ })
File without changes
@@ -0,0 +1,70 @@
1
+ """OpenAI Responses API 호출을 인터셉트하는 모듈."""
2
+ from openai import OpenAIError
3
+
4
+ from whatap.llm.providers.interceptor import (
5
+ before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
+ capture_client,
7
+ )
8
+ from whatap.llm.providers.stream_accumulator import sync_stream, async_stream
9
+ from whatap.llm.providers.openai.responses.responses_context import build_context
10
+ from whatap.llm.providers.openai.responses.responses_extractor import finalize, ResponsesStream
11
+
12
+
13
+ def intercept_responses_create(fn, *args, **kwargs):
14
+ """OpenAI Responses 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
15
+ pack, ctx, features, stream = build_context(kwargs)
16
+ capture_client(pack, ctx, args)
17
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
18
+
19
+ before_call(pack, active_key)
20
+ _stream_returned = False
21
+ try:
22
+ try:
23
+ response = fn(*args, **kwargs)
24
+ except Exception as err:
25
+ handle_error(pack, err, active_key, OpenAIError)
26
+ raise
27
+ finally:
28
+ if ctx:
29
+ ctx._llm_httpc_pending = False
30
+
31
+ after_call(pack, ctx)
32
+ if stream:
33
+ _stream_returned = True
34
+ return sync_stream(response, ResponsesStream(pack, active_key))
35
+ finalize(response, pack, features)
36
+ finalize_non_streaming(pack, active_key)
37
+ return response
38
+ finally:
39
+ if not _stream_returned:
40
+ _ensure_end(pack, active_key)
41
+
42
+
43
+ async def intercept_responses_create_async(fn, *args, **kwargs):
44
+ """OpenAI Responses 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
45
+ pack, ctx, features, stream = build_context(kwargs)
46
+ capture_client(pack, ctx, args)
47
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
48
+
49
+ before_call(pack, active_key)
50
+ _stream_returned = False
51
+ try:
52
+ try:
53
+ response = await fn(*args, **kwargs)
54
+ except Exception as err:
55
+ handle_error(pack, err, active_key, OpenAIError)
56
+ raise
57
+ finally:
58
+ if ctx:
59
+ ctx._llm_httpc_pending = False
60
+
61
+ after_call(pack, ctx)
62
+ if stream:
63
+ _stream_returned = True
64
+ return async_stream(response, ResponsesStream(pack, active_key))
65
+ finalize(response, pack, features)
66
+ finalize_non_streaming(pack, active_key)
67
+ return response
68
+ finally:
69
+ if not _stream_returned:
70
+ _ensure_end(pack, active_key)
@@ -0,0 +1,88 @@
1
+ """OpenAI Responses API 요청에서 컨텍스트를 구성하는 모듈."""
2
+ import json
3
+
4
+ from whatap import logging
5
+ from whatap.llm.features import LlmFeature
6
+ from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
7
+ from whatap.llm.providers.openai.content_parser import extract_content_text, _block_to_dict
8
+ from whatap.trace.trace_context_manager import TraceContextManager
9
+
10
+
11
+ def _msg_to_dict(msg):
12
+ """메시지 객체를 딕셔너리로 변환한다."""
13
+ if isinstance(msg, dict):
14
+ return msg
15
+ if hasattr(msg, 'model_dump'):
16
+ try:
17
+ return msg.model_dump()
18
+ except Exception as e:
19
+ logging.warning('[LLM] _msg_to_dict model_dump failed: %s' % e, extra={'id': 'LLM014'})
20
+ if hasattr(msg, '__dict__'):
21
+ return dict(vars(msg))
22
+ return {"type": str(getattr(msg, 'type', 'unknown'))}
23
+
24
+
25
+ def build_context(kwargs):
26
+ """Responses kwargs로부터 LlmStepStatus 팩과 트레이스 컨텍스트를 구성한다."""
27
+ input_data = kwargs.get("input", "")
28
+ model = kwargs.get("model")
29
+ stream = kwargs.get("stream", False)
30
+ instructions = kwargs.get("instructions")
31
+
32
+ system_parts = [instructions] if instructions else []
33
+ has_vision = False
34
+ tool_results = []
35
+
36
+ if isinstance(input_data, str):
37
+ prompt_text = input_data
38
+ elif isinstance(input_data, list):
39
+ prompt_msgs = []
40
+ for raw_msg in input_data:
41
+ msg = _msg_to_dict(raw_msg)
42
+ role = msg.get("role", "user")
43
+ content = msg.get("content")
44
+ if role == "system":
45
+ system_parts.append(content or "")
46
+ else:
47
+ sanitized = dict(msg)
48
+ if isinstance(content, list):
49
+ if not has_vision:
50
+ for block in content:
51
+ if _block_to_dict(block).get("type") == "input_image":
52
+ has_vision = True
53
+ break
54
+ sanitized["content"] = extract_content_text(content)
55
+ prompt_msgs.append(sanitized)
56
+ if msg.get("type") == "function_call_output":
57
+ tool_results.append({"call_id": msg.get("call_id", ""), "output": msg.get("output", "")})
58
+ try:
59
+ prompt_text = json.dumps(prompt_msgs, ensure_ascii=False) if prompt_msgs else ""
60
+ except Exception as e:
61
+ logging.warning('[LLM] responses prompt serialization failed: %s' % e, extra={'id': 'LLM005'})
62
+ prompt_text = ""
63
+ else:
64
+ prompt_text = str(input_data)
65
+
66
+ features = []
67
+ if kwargs.get("reasoning"):
68
+ features.append(LlmFeature.REASONING)
69
+ if has_vision:
70
+ features.append(LlmFeature.VISION)
71
+
72
+ pack = LlmStepStatus()
73
+ pack.model = model
74
+ pack.prompt_text = prompt_text
75
+ pack.system_texts = system_parts
76
+ pack.stream = stream
77
+ pack.features = ",".join(features)
78
+ if kwargs.get("temperature") is not None:
79
+ pack.temperature = kwargs["temperature"]
80
+ if tool_results:
81
+ pack.tool_results_text = json.dumps(tool_results, ensure_ascii=False)
82
+
83
+ ctx = TraceContextManager.getLocalContext()
84
+ if ctx:
85
+ ctx._llm_httpc_pending = True
86
+ ctx._llm_model = model
87
+
88
+ return pack, ctx, features, stream