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
whatap/llm/pricing.py ADDED
@@ -0,0 +1,236 @@
1
+ """LLM 모델별 가격 데이터 및 비용 계산 로직.
2
+
3
+ genai-prices 패키지가 설치되어 있으면 우선 사용하고,
4
+ 없으면 하드코딩된 MODEL_PRICING으로 fallback한다.
5
+ """
6
+ from whatap.conf.configure import Configure as conf
7
+
8
+ try:
9
+ from genai_prices import Usage as _GenaiUsage, calc_price as _genai_calc_price
10
+ _HAS_GENAI_PRICES = True
11
+ except ImportError:
12
+ _HAS_GENAI_PRICES = False
13
+
14
+ # fallback: USD per 1M tokens (input, output, cached_input)
15
+ MODEL_PRICING = {
16
+ # OpenAI - GPT-5
17
+ 'gpt-5': (1.25, 10.00, 0.125),
18
+ 'gpt-5-mini': (0.25, 2.00, 0.025),
19
+ 'gpt-5-nano': (0.05, 0.40, 0.005),
20
+ 'gpt-5.1': (1.25, 10.00, 0.125),
21
+ 'gpt-5.2': (1.75, 14.00, 0.175),
22
+ # OpenAI - GPT-4
23
+ 'gpt-4o': (2.50, 10.00, 1.25),
24
+ 'gpt-4o-mini': (0.15, 0.60, 0.075),
25
+ 'gpt-4o-audio-preview': (2.50, 10.00, None),
26
+ 'gpt-4o-mini-audio-preview': (0.15, 0.60, None),
27
+ 'gpt-4.1': (2.00, 8.00, 0.50),
28
+ 'gpt-4.1-mini': (0.40, 1.60, 0.10),
29
+ 'gpt-4.1-nano': (0.10, 0.40, 0.025),
30
+ 'gpt-4-turbo': (10.00, 30.00, None),
31
+ 'gpt-4': (30.00, 60.00, None),
32
+ 'gpt-3.5-turbo': (0.50, 1.50, None),
33
+ # OpenAI - o
34
+ 'o1': (15.00, 60.00, 7.50),
35
+ 'o1-pro': (150.00, 600.00, None),
36
+ 'o3': (2.00, 8.00, 0.50),
37
+ 'o3-mini': (1.10, 4.40, 0.55),
38
+ 'o3-pro': (20.00, 80.00, None),
39
+ 'o4-mini': (1.10, 4.40, 0.275),
40
+ # Anthropic - Claude 4.x
41
+ 'claude-opus-4-6': (5.00, 25.00, 0.50),
42
+ 'claude-sonnet-4-6': (3.00, 15.00, 0.30),
43
+ 'claude-opus-4-5-20251101': (5.00, 25.00, 0.50),
44
+ 'claude-opus-4-1-20250805': (15.00, 75.00, 1.50),
45
+ 'claude-sonnet-4-5-20250929': (3.00, 15.00, 0.30),
46
+ 'claude-sonnet-4-20250514': (3.00, 15.00, 0.30),
47
+ 'claude-opus-4-20250514': (15.00, 75.00, 1.50),
48
+ 'claude-haiku-4-5-20251001': (1.00, 5.00, 0.10),
49
+ # Anthropic - Claude 3.x
50
+ 'claude-3-5-sonnet-20241022': (3.00, 15.00, 0.30),
51
+ 'claude-3-5-sonnet-20240620': (3.00, 15.00, 0.30),
52
+ 'claude-3-5-haiku-20241022': (0.80, 4.00, 0.08),
53
+ 'claude-3-opus-20240229': (15.00, 75.00, 1.50),
54
+ 'claude-3-sonnet-20240229': (3.00, 15.00, 0.30),
55
+ 'claude-3-haiku-20240307': (0.25, 1.25, 0.03),
56
+ # OpenAI - Embeddings
57
+ 'text-embedding-3-small': (0.02, 0, None),
58
+ 'text-embedding-3-large': (0.13, 0, None),
59
+ 'text-embedding-ada-002': (0.10, 0, None),
60
+ }
61
+
62
+ # prefix match fallback (model ID variations)
63
+ MODEL_PRICING_PREFIX = [
64
+ # OpenAI - GPT-5
65
+ ('gpt-5.2', (1.75, 14.00, 0.175)),
66
+ ('gpt-5.1', (1.25, 10.00, 0.125)),
67
+ ('gpt-5-nano', (0.05, 0.40, 0.005)),
68
+ ('gpt-5-mini', (0.25, 2.00, 0.025)),
69
+ ('gpt-5', (1.25, 10.00, 0.125)),
70
+ # OpenAI - GPT-4
71
+ ('gpt-4o-mini', (0.15, 0.60, 0.075)),
72
+ ('gpt-4o', (2.50, 10.00, 1.25)),
73
+ ('gpt-4.1-nano', (0.10, 0.40, 0.025)),
74
+ ('gpt-4.1-mini', (0.40, 1.60, 0.10)),
75
+ ('gpt-4.1', (2.00, 8.00, 0.50)),
76
+ ('gpt-4-turbo', (10.00, 30.00, None)),
77
+ ('gpt-4', (30.00, 60.00, None)),
78
+ ('gpt-3.5-turbo', (0.50, 1.50, None)),
79
+ # OpenAI - o
80
+ ('o4-mini', (1.10, 4.40, 0.275)),
81
+ ('o3-pro', (20.00, 80.00, None)),
82
+ ('o3-mini', (1.10, 4.40, 0.55)),
83
+ ('o3', (2.00, 8.00, 0.50)),
84
+ ('o1-pro', (150.00, 600.00, None)),
85
+ ('o1', (15.00, 60.00, 7.50)),
86
+ # Anthropic
87
+ ('claude-opus-4-6', (5.00, 25.00, 0.50)),
88
+ ('claude-sonnet-4-6', (3.00, 15.00, 0.30)),
89
+ ('claude-opus-4-5', (5.00, 25.00, 0.50)),
90
+ ('claude-opus-4-1', (15.00, 75.00, 1.50)),
91
+ ('claude-sonnet-4-5', (3.00, 15.00, 0.30)),
92
+ ('claude-sonnet-4', (3.00, 15.00, 0.30)),
93
+ ('claude-opus-4', (15.00, 75.00, 1.50)),
94
+ ('claude-haiku-4-5', (1.00, 5.00, 0.10)),
95
+ ('claude-3-5-sonnet', (3.00, 15.00, 0.30)),
96
+ ('claude-3-5-haiku', (0.80, 4.00, 0.08)),
97
+ ('claude-3-opus', (15.00, 75.00, 1.50)),
98
+ ('claude-3-sonnet', (3.00, 15.00, 0.30)),
99
+ ('claude-3-haiku', (0.25, 1.25, 0.03)),
100
+ # OpenAI - Embeddings
101
+ ('text-embedding-3-small', (0.02, 0, None)),
102
+ ('text-embedding-3-large', (0.13, 0, None)),
103
+ ('text-embedding-ada', (0.10, 0, None)),
104
+ ]
105
+
106
+ _custom_pricing_cache = {}
107
+ _custom_pricing_raw = None
108
+
109
+
110
+ def _parse_custom_pricing():
111
+ global _custom_pricing_cache, _custom_pricing_raw
112
+ raw = getattr(conf, 'llm_model_pricing', '') or ''
113
+ if raw == _custom_pricing_raw:
114
+ return _custom_pricing_cache
115
+ _custom_pricing_raw = raw
116
+ if not raw:
117
+ _custom_pricing_cache = {}
118
+ return _custom_pricing_cache
119
+ pricing = {}
120
+ for entry in raw.split(','):
121
+ parts = entry.strip().split('|')
122
+ if len(parts) < 3:
123
+ continue
124
+ model = parts[0].strip()
125
+ try:
126
+ inp = float(parts[1].strip())
127
+ out = float(parts[2].strip())
128
+ cached = float(parts[3].strip()) if len(parts) > 3 and parts[3].strip() else None
129
+ pricing[model] = (inp, out, cached)
130
+ except (ValueError, IndexError):
131
+ continue
132
+ _custom_pricing_cache = pricing
133
+ return _custom_pricing_cache
134
+
135
+
136
+ def _get_fallback_pricing(model):
137
+ """하드코딩 테이블에서 모델 가격 조회. exact match → prefix match."""
138
+ pricing = MODEL_PRICING.get(model)
139
+ if pricing:
140
+ return pricing
141
+ for prefix, p in MODEL_PRICING_PREFIX:
142
+ if model.startswith(prefix):
143
+ return p
144
+ return None
145
+
146
+
147
+ def _detect_provider(pack):
148
+ """pack.provider에서 genai-prices provider_id를 추출."""
149
+ provider = (pack.provider or '').lower()
150
+ if 'openai' in provider or 'azure' in provider:
151
+ return 'openai'
152
+ if 'anthropic' in provider:
153
+ return 'anthropic'
154
+ if 'google' in provider or 'gemini' in provider:
155
+ return 'google'
156
+ if 'deepseek' in provider:
157
+ return 'deepseek'
158
+ if 'mistral' in provider:
159
+ return 'mistral'
160
+ return None
161
+
162
+
163
+ def calculate_cost(pack):
164
+ """팩의 모델과 토큰 수를 기반으로 비용을 계산하여 팩에 설정한다.
165
+
166
+ genai-prices 우선 → custom config → 하드코딩 fallback 순서.
167
+ """
168
+ if not pack.model:
169
+ return
170
+
171
+ # 1. custom config 우선
172
+ custom = _parse_custom_pricing()
173
+ if custom:
174
+ pricing = custom.get(pack.model)
175
+ if pricing:
176
+ _calculate_from_tuple(pack, pricing)
177
+ return
178
+
179
+ # 2. genai-prices 패키지
180
+ if _HAS_GENAI_PRICES:
181
+ if _calculate_from_genai(pack):
182
+ return
183
+
184
+ # 3. 하드코딩 fallback
185
+ pricing = _get_fallback_pricing(pack.model)
186
+ if pricing:
187
+ _calculate_from_tuple(pack, pricing)
188
+
189
+
190
+ def _calculate_from_genai(pack):
191
+ """genai-prices로 비용 계산. 성공 시 True, 모델 없으면 False."""
192
+ try:
193
+ usage = _GenaiUsage(
194
+ input_tokens=pack.input_tokens or 0,
195
+ output_tokens=pack.output_tokens or 0,
196
+ cache_read_tokens=(pack.cached_tokens or 0) + (pack.cache_read_input_tokens or 0),
197
+ cache_write_tokens=pack.cache_creation_input_tokens or 0,
198
+ )
199
+ provider_id = _detect_provider(pack)
200
+ kwargs = {'model_ref': pack.model}
201
+ if provider_id:
202
+ kwargs['provider_id'] = provider_id
203
+
204
+ result = _genai_calc_price(usage, **kwargs)
205
+
206
+ pack.input_cost = round(float(result.input_price), 6)
207
+ pack.output_cost = round(float(result.output_price), 6)
208
+ pack.cost = round(float(result.total_price), 6)
209
+ pack.cached_cost = 0.0
210
+ return True
211
+ except Exception:
212
+ return False
213
+
214
+
215
+ def _calculate_from_tuple(pack, pricing):
216
+ """(input_per_1m, output_per_1m, cached_per_1m) 튜플로 비용 계산."""
217
+ input_price, output_price, cached_price = pricing
218
+
219
+ input_count = pack.input_tokens or 0
220
+ output_count = pack.output_tokens or 0
221
+ cached = (pack.cached_tokens or 0) + (pack.cache_read_input_tokens or 0)
222
+
223
+ if cached_price is not None and cached > 0:
224
+ non_cached = max(input_count - cached, 0)
225
+ cached_cost = (cached * cached_price) / 1_000_000
226
+ input_cost = (non_cached * input_price) / 1_000_000 + cached_cost
227
+ else:
228
+ input_cost = (input_count * input_price) / 1_000_000
229
+ cached_cost = 0.0
230
+
231
+ output_cost = (output_count * output_price) / 1_000_000
232
+
233
+ pack.input_cost = round(input_cost, 6)
234
+ pack.output_cost = round(output_cost, 6)
235
+ pack.cached_cost = round(cached_cost, 6)
236
+ pack.cost = round(input_cost + output_cost, 6)
@@ -0,0 +1,288 @@
1
+ """LLM 호출의 prompt 메타 (operation_type / prompt_version) 를 모든 logsink pack 의
2
+ 공통 태그로 자동 인라인.
3
+
4
+ 배경: 사용자가 LLM API 를 어떤 프롬프트 / 체인 / agent 로 호출했는지 백엔드에서 그룹/필터링
5
+ 하기 위한 라벨링. 백엔드 대시보드의 ``operation_type`` 태그를 그대로 활용 (이전엔
6
+ 'chat'/'embedding' 같은 API 종류였던 자리).
7
+
8
+ 사용법 (데코레이터):
9
+
10
+ from whatap.llm.prompt_meta import prompt_meta
11
+
12
+ @prompt_meta(operation_type='checkout_chain', prompt_version='v3')
13
+ def checkout(question):
14
+ return client.chat.completions.create(...)
15
+
16
+ 또는 컨텍스트 매니저:
17
+
18
+ from whatap.llm.prompt_meta import prompt_meta_scope
19
+
20
+ def chat(question):
21
+ with prompt_meta_scope(operation_type='greeting', prompt_version='v2'):
22
+ return client.chat.completions.create(...)
23
+
24
+ 데코레이터/스코프 미적용 시 기본값:
25
+ operation_type='default', prompt_version='v1'
26
+
27
+ LangChain / LlamaIndex 등 AI 프레임워크 계측 시에도 framework 의 자연스러운 단위
28
+ (chain 이름 / agent 이름 등) 를 ``operation_type`` 으로 자동 채워줄 수 있다.
29
+
30
+ 저장: trace context (있으면) + ContextVar (항상). 양쪽 동시에 같은 stack 을 push/pop.
31
+ 중첩 안전 (LIFO stack). asyncio.create_task / TaskGroup 분기 시 ContextVar 가 inherit
32
+ 되어 sub-task 안에서 LLM 호출 시에도 같은 prompt_meta 가 보임.
33
+ ``whatap.llm.evaluators.scope`` 와 같은 패턴.
34
+ """
35
+ import asyncio
36
+ import contextvars
37
+ import functools
38
+ import inspect
39
+
40
+ from whatap import logging
41
+
42
+
43
+ # task 별로 격리되는 storage. asyncio.create_task / TaskGroup 으로 분기된 task 도
44
+ # 시작 시점의 ContextVar snapshot 을 inherit. None 이 default.
45
+ _meta_cv = contextvars.ContextVar('whatap_llm_prompt_meta_stack', default=None)
46
+
47
+ DEFAULT_OPERATION_TYPE = 'default'
48
+ DEFAULT_PROMPT_VERSION = 'v1'
49
+
50
+
51
+ def _ensure_cv_stack():
52
+ cur = _meta_cv.get()
53
+ if cur is None:
54
+ cur = []
55
+ _meta_cv.set(cur)
56
+ return cur
57
+
58
+
59
+ def _ensure_ctx_stack(ctx):
60
+ if not hasattr(ctx, '_llm_prompt_meta_stack'):
61
+ ctx._llm_prompt_meta_stack = []
62
+ return ctx._llm_prompt_meta_stack
63
+
64
+
65
+ def _get_active_stacks():
66
+ """현재 활성 stack 들 — (trace ctx + ContextVar) 양쪽 모두 반환.
67
+
68
+ enter 시 양쪽 모두에 push, exit 시 양쪽 모두에서 pop. trace ctx 가 hot path 에서
69
+ 빠른 lookup, ContextVar 는 task 분기 시 inherit 보강.
70
+ """
71
+ stacks = []
72
+ try:
73
+ from whatap.trace.trace_context_manager import TraceContextManager
74
+ ctx = TraceContextManager.getLocalContext()
75
+ if ctx is not None:
76
+ stacks.append(_ensure_ctx_stack(ctx))
77
+ except Exception:
78
+ pass
79
+ stacks.append(_ensure_cv_stack())
80
+ return stacks
81
+
82
+
83
+ def get_prompt_meta():
84
+ """현재 활성 ``(operation_type, prompt_version)`` 튜플 반환.
85
+
86
+ 스코프 미적용 시 ``('default', 'v1')``. 중첩 스코프이면 가장 안쪽 (stack top) 값.
87
+ interceptor 가 LLM 호출 시점에 호출해 pack 의 태그 set.
88
+
89
+ trace ctx stack 우선 — 같은 task chain 이면 ContextVar 와 자동 sync 되어 같은 값.
90
+ trace ctx 가 None 이거나 다른 ctx 인 경우 ContextVar fallback.
91
+ """
92
+ # trace ctx 우선
93
+ try:
94
+ from whatap.trace.trace_context_manager import TraceContextManager
95
+ ctx = TraceContextManager.getLocalContext()
96
+ if ctx is not None:
97
+ stack = getattr(ctx, '_llm_prompt_meta_stack', None)
98
+ if stack:
99
+ return stack[-1]
100
+ except Exception:
101
+ pass
102
+
103
+ cv_stack = _meta_cv.get()
104
+ if cv_stack:
105
+ return cv_stack[-1]
106
+
107
+ return DEFAULT_OPERATION_TYPE, DEFAULT_PROMPT_VERSION
108
+
109
+
110
+ class prompt_meta_scope(object):
111
+ """LLM 호출에 prompt 메타데이터를 스코프 단위로 적용하는 컨텍스트 매니저.
112
+
113
+ Example:
114
+ with prompt_meta_scope(operation_type='greeting', prompt_version='v2'):
115
+ response = client.chat.completions.create(...)
116
+
117
+ 중첩 안전 — 안쪽 스코프 가 stack top 으로, 안쪽 호출에만 적용. 바깥 스코프 보존.
118
+ """
119
+
120
+ def __init__(self, operation_type=None, prompt_version=None):
121
+ self._op = str(operation_type) if operation_type else DEFAULT_OPERATION_TYPE
122
+ self._ver = str(prompt_version) if prompt_version else DEFAULT_PROMPT_VERSION
123
+ self._stacks = None
124
+ self._pushed = False
125
+
126
+ def __enter__(self):
127
+ try:
128
+ self._stacks = _get_active_stacks()
129
+ for s in self._stacks:
130
+ s.append((self._op, self._ver))
131
+ self._pushed = True
132
+ except Exception as e:
133
+ logging.warning('[LLM] prompt_meta_scope enter failed: %s' % e,
134
+ extra={'id': 'LLM070'})
135
+ return self
136
+
137
+ def __exit__(self, exc_type, exc_val, exc_tb):
138
+ if not self._pushed:
139
+ return False
140
+ try:
141
+ for s in self._stacks or ():
142
+ if s and s[-1] == (self._op, self._ver):
143
+ s.pop()
144
+ elif s:
145
+ # stack 망쳐졌으면 (외부 leak 등) 가장 가까운 같은 항목만 제거 시도
146
+ for i in range(len(s) - 1, -1, -1):
147
+ if s[i] == (self._op, self._ver):
148
+ del s[i]
149
+ break
150
+ except Exception as e:
151
+ logging.warning('[LLM] prompt_meta_scope exit failed: %s' % e,
152
+ extra={'id': 'LLM071'})
153
+ finally:
154
+ self._stacks = None
155
+ self._pushed = False
156
+ return False # 예외 propagate
157
+
158
+
159
+ def _is_streaming_response(obj):
160
+ """FastAPI/Starlette StreamingResponse 호환 객체 감지."""
161
+ return hasattr(obj, 'body_iterator')
162
+
163
+
164
+ def _wrap_async_iter(orig_iter, scope):
165
+ async def wrapped():
166
+ try:
167
+ async for chunk in orig_iter:
168
+ yield chunk
169
+ finally:
170
+ scope.__exit__(None, None, None)
171
+ return wrapped()
172
+
173
+
174
+ def _wrap_sync_iter(orig_iter, scope):
175
+ def wrapped():
176
+ try:
177
+ for chunk in orig_iter:
178
+ yield chunk
179
+ finally:
180
+ scope.__exit__(None, None, None)
181
+ return wrapped()
182
+
183
+
184
+ def _wrap_result_keep_scope(result, scope):
185
+ """함수 result 의 type 별로 scope 유지 wrapping.
186
+
187
+ - StreamingResponse → body_iterator wrap
188
+ - async generator object / sync generator object → 직접 wrap
189
+ - 그 외 → 즉시 scope 닫음
190
+ """
191
+ if _is_streaming_response(result):
192
+ orig = result.body_iterator
193
+ if inspect.isasyncgen(orig) or hasattr(orig, '__aiter__'):
194
+ result.body_iterator = _wrap_async_iter(orig, scope)
195
+ else:
196
+ result.body_iterator = _wrap_sync_iter(orig, scope)
197
+ return result
198
+
199
+ if inspect.isasyncgen(result):
200
+ return _wrap_async_iter(result, scope)
201
+
202
+ if inspect.isgenerator(result):
203
+ return _wrap_sync_iter(result, scope)
204
+
205
+ scope.__exit__(None, None, None)
206
+ return result
207
+
208
+
209
+ def prompt_meta(operation_type=None, prompt_version=None):
210
+ """함수에 prompt 메타데이터 (operation_type / prompt_version) 를 스코프 단위로
211
+ 묶는 데코레이터.
212
+
213
+ sync / async / generator / async generator / FastAPI StreamingResponse 모두 지원 —
214
+ 함수 안에서 발생하는 LLM 호출의 메트릭/pack 태그에 자동 인라인.
215
+
216
+ Example:
217
+ @prompt_meta(operation_type='greeting', prompt_version='v1')
218
+ async def chat(q):
219
+ return await client.chat.completions.create(...)
220
+
221
+ @prompt_meta(operation_type='rag_chain', prompt_version='v3')
222
+ async def rag(q):
223
+ async def gen():
224
+ stream = await client.chat.completions.create(stream=True, ...)
225
+ async for chunk in stream:
226
+ yield chunk
227
+ return StreamingResponse(gen(), ...)
228
+
229
+ asyncio.create_task / TaskGroup 으로 분기된 sub-task 도 ContextVar inherit 으로
230
+ 같은 prompt_meta 가 보임.
231
+ """
232
+ op = operation_type
233
+ ver = prompt_version
234
+
235
+ def decorator(fn):
236
+ if asyncio.iscoroutinefunction(fn):
237
+ @functools.wraps(fn)
238
+ async def async_wrapper(*args, **kwargs):
239
+ scope = prompt_meta_scope(operation_type=op, prompt_version=ver)
240
+ scope.__enter__()
241
+ try:
242
+ result = await fn(*args, **kwargs)
243
+ except BaseException:
244
+ scope.__exit__(None, None, None)
245
+ raise
246
+ return _wrap_result_keep_scope(result, scope)
247
+ return async_wrapper
248
+
249
+ if inspect.isasyncgenfunction(fn):
250
+ @functools.wraps(fn)
251
+ def async_gen_wrapper(*args, **kwargs):
252
+ scope = prompt_meta_scope(operation_type=op, prompt_version=ver)
253
+ scope.__enter__()
254
+ try:
255
+ gen = fn(*args, **kwargs)
256
+ except BaseException:
257
+ scope.__exit__(None, None, None)
258
+ raise
259
+ return _wrap_async_iter(gen, scope)
260
+ return async_gen_wrapper
261
+
262
+ if inspect.isgeneratorfunction(fn):
263
+ @functools.wraps(fn)
264
+ def gen_wrapper(*args, **kwargs):
265
+ scope = prompt_meta_scope(operation_type=op, prompt_version=ver)
266
+ scope.__enter__()
267
+ try:
268
+ gen = fn(*args, **kwargs)
269
+ except BaseException:
270
+ scope.__exit__(None, None, None)
271
+ raise
272
+ return _wrap_sync_iter(gen, scope)
273
+ return gen_wrapper
274
+
275
+ # 일반 sync 함수 — return 이 generator/asyncgen/StreamingResponse 일 수 있으니
276
+ # result 분기 적용.
277
+ @functools.wraps(fn)
278
+ def sync_wrapper(*args, **kwargs):
279
+ scope = prompt_meta_scope(operation_type=op, prompt_version=ver)
280
+ scope.__enter__()
281
+ try:
282
+ result = fn(*args, **kwargs)
283
+ except BaseException:
284
+ scope.__exit__(None, None, None)
285
+ raise
286
+ return _wrap_result_keep_scope(result, scope)
287
+ return sync_wrapper
288
+ return decorator
File without changes
@@ -0,0 +1,37 @@
1
+ from whatap.conf.configure import Configure as conf
2
+ from whatap.trace.trace_handler import trace_handler, async_trace_handler
3
+ from whatap.llm.providers.anthropic.messages.messages import intercept_create, async_intercept_create
4
+
5
+
6
+ def instrument_anthropic(module):
7
+
8
+ if not conf.llm_enabled:
9
+ return
10
+
11
+ def create_wrapper(fn):
12
+ @trace_handler(fn)
13
+ def trace(*args, **kwargs):
14
+ return intercept_create(fn, *args, **kwargs)
15
+ return trace
16
+
17
+ def async_create_wrapper(fn):
18
+ @async_trace_handler(fn)
19
+ async def trace(*args, **kwargs):
20
+ return await async_intercept_create(fn, *args, **kwargs)
21
+ return trace
22
+
23
+ # Sync: anthropic.resources.messages.Messages.create
24
+ if (hasattr(module, 'resources') and
25
+ hasattr(module.resources, 'messages') and
26
+ hasattr(module.resources.messages, 'Messages') and
27
+ hasattr(module.resources.messages.Messages, 'create')):
28
+ original_create = module.resources.messages.Messages.create
29
+ module.resources.messages.Messages.create = create_wrapper(original_create)
30
+
31
+ # Async: anthropic.resources.messages.AsyncMessages.create
32
+ if (hasattr(module, 'resources') and
33
+ hasattr(module.resources, 'messages') and
34
+ hasattr(module.resources.messages, 'AsyncMessages') and
35
+ hasattr(module.resources.messages.AsyncMessages, 'create')):
36
+ original_async_create = module.resources.messages.AsyncMessages.create
37
+ module.resources.messages.AsyncMessages.create = async_create_wrapper(original_async_create)
File without changes
@@ -0,0 +1,70 @@
1
+ """Anthropic Messages API 호출을 인터셉트하는 모듈."""
2
+ from anthropic import APIError
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.anthropic.messages.messages_context import build_context
10
+ from whatap.llm.providers.anthropic.messages.messages_extractor import finalize, AnthropicStream
11
+
12
+
13
+ def intercept_create(fn, *args, **kwargs):
14
+ """Anthropic Messages 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
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, APIError)
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, AnthropicStream(pack, active_key, features))
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 async_intercept_create(fn, *args, **kwargs):
44
+ """Anthropic Messages 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
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, APIError)
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, AnthropicStream(pack, active_key, features))
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)