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.
- whatap/LICENSE +0 -0
- whatap/README.rst +49 -0
- whatap/__init__.py +923 -0
- whatap/__main__.py +4 -0
- whatap/agent/darwin/amd64/whatap_python +0 -0
- whatap/agent/darwin/arm64/whatap_python +0 -0
- whatap/agent/linux/amd64/whatap_python +0 -0
- whatap/agent/linux/arm64/whatap_python +0 -0
- whatap/agent/windows/whatap_python.exe +0 -0
- whatap/bootstrap/__init__.py +0 -0
- whatap/bootstrap/sitecustomize.py +19 -0
- whatap/build.py +4 -0
- whatap/conf/__init__.py +0 -0
- whatap/conf/configuration.py +280 -0
- whatap/conf/configure.py +105 -0
- whatap/conf/license.py +49 -0
- whatap/control/__init__.py +0 -0
- whatap/counter/__init__.py +14 -0
- whatap/counter/counter_manager.py +45 -0
- whatap/counter/tasks/__init__.py +3 -0
- whatap/counter/tasks/base_task.py +26 -0
- whatap/counter/tasks/llm_evaluator_task.py +501 -0
- whatap/counter/tasks/llm_log_sink_task.py +309 -0
- whatap/counter/tasks/llm_stat_task.py +78 -0
- whatap/counter/tasks/openfiledescriptor.py +67 -0
- whatap/io/__init__.py +1 -0
- whatap/io/data_inputx.py +161 -0
- whatap/io/data_outputx.py +262 -0
- whatap/llm/__init__.py +17 -0
- whatap/llm/definitions.py +43 -0
- whatap/llm/evaluators/__init__.py +136 -0
- whatap/llm/evaluators/base.py +114 -0
- whatap/llm/evaluators/builtins/__init__.py +91 -0
- whatap/llm/evaluators/builtins/answer_relevance.py +46 -0
- whatap/llm/evaluators/builtins/combined_judge.py +271 -0
- whatap/llm/evaluators/builtins/factuality.py +71 -0
- whatap/llm/evaluators/builtins/hallucination.py +97 -0
- whatap/llm/evaluators/builtins/llm_judge.py +516 -0
- whatap/llm/evaluators/builtins/pii_leak.py +214 -0
- whatap/llm/evaluators/builtins/prompt_injection.py +71 -0
- whatap/llm/evaluators/builtins/toxicity.py +53 -0
- whatap/llm/evaluators/builtins/url_scan.py +194 -0
- whatap/llm/evaluators/registry.py +192 -0
- whatap/llm/evaluators/sampler.py +83 -0
- whatap/llm/evaluators/scope.py +334 -0
- whatap/llm/features.py +66 -0
- whatap/llm/log_sink_packs/__init__.py +9 -0
- whatap/llm/log_sink_packs/llm_input_message.py +16 -0
- whatap/llm/log_sink_packs/llm_log_sink_pack.py +72 -0
- whatap/llm/log_sink_packs/llm_output_message.py +19 -0
- whatap/llm/log_sink_packs/llm_step_eval_status.py +94 -0
- whatap/llm/log_sink_packs/llm_step_status.py +118 -0
- whatap/llm/log_sink_packs/llm_system_message.py +16 -0
- whatap/llm/log_sink_packs/llm_tool_calls.py +44 -0
- whatap/llm/log_sink_packs/llm_tool_results.py +16 -0
- whatap/llm/log_sink_packs/llm_tx_status.py +108 -0
- whatap/llm/pricing.py +236 -0
- whatap/llm/prompt_meta.py +288 -0
- whatap/llm/providers/__init__.py +0 -0
- whatap/llm/providers/anthropic/__init__.py +37 -0
- whatap/llm/providers/anthropic/messages/__init__.py +0 -0
- whatap/llm/providers/anthropic/messages/messages.py +70 -0
- whatap/llm/providers/anthropic/messages/messages_context.py +76 -0
- whatap/llm/providers/anthropic/messages/messages_extractor.py +126 -0
- whatap/llm/providers/interceptor.py +182 -0
- whatap/llm/providers/openai/__init__.py +133 -0
- whatap/llm/providers/openai/chat/__init__.py +0 -0
- whatap/llm/providers/openai/chat/chat.py +82 -0
- whatap/llm/providers/openai/chat/chat_context.py +78 -0
- whatap/llm/providers/openai/chat/chat_extractor.py +127 -0
- whatap/llm/providers/openai/completions/__init__.py +0 -0
- whatap/llm/providers/openai/completions/completions.py +70 -0
- whatap/llm/providers/openai/completions/completions_context.py +31 -0
- whatap/llm/providers/openai/completions/completions_extractor.py +61 -0
- whatap/llm/providers/openai/content_parser.py +41 -0
- whatap/llm/providers/openai/embeddings/__init__.py +0 -0
- whatap/llm/providers/openai/embeddings/embeddings.py +59 -0
- whatap/llm/providers/openai/embeddings/embeddings_context.py +25 -0
- whatap/llm/providers/openai/embeddings/embeddings_extractor.py +26 -0
- whatap/llm/providers/openai/responses/__init__.py +0 -0
- whatap/llm/providers/openai/responses/responses.py +70 -0
- whatap/llm/providers/openai/responses/responses_context.py +88 -0
- whatap/llm/providers/openai/responses/responses_extractor.py +126 -0
- whatap/llm/providers/stream_accumulator.py +73 -0
- whatap/llm/stats/__init__.py +35 -0
- whatap/llm/stats/active_stat.py +86 -0
- whatap/llm/stats/answer_relevance_eval_stat.py +10 -0
- whatap/llm/stats/api_status_stat.py +35 -0
- whatap/llm/stats/base_stat.py +107 -0
- whatap/llm/stats/combined_judge_eval_stat.py +11 -0
- whatap/llm/stats/error_stat.py +59 -0
- whatap/llm/stats/eval_stat.py +225 -0
- whatap/llm/stats/factuality_eval_stat.py +10 -0
- whatap/llm/stats/feature_stat.py +104 -0
- whatap/llm/stats/finish_stat.py +105 -0
- whatap/llm/stats/hallucination_eval_stat.py +10 -0
- whatap/llm/stats/meter.py +18 -0
- whatap/llm/stats/perf_stat.py +117 -0
- whatap/llm/stats/pii_leak_eval_stat.py +12 -0
- whatap/llm/stats/prompt_injection_eval_stat.py +10 -0
- whatap/llm/stats/token_usage_stat.py +133 -0
- whatap/llm/stats/toxicity_eval_stat.py +10 -0
- whatap/llm/stats/url_scan_eval_stat.py +12 -0
- whatap/net/__init__.py +0 -0
- whatap/net/async_sender.py +107 -0
- whatap/net/packet_enum.py +44 -0
- whatap/net/packet_type_enum.py +31 -0
- whatap/net/param_def.py +69 -0
- whatap/net/stackhelper.py +87 -0
- whatap/net/udp_session.py +394 -0
- whatap/net/udp_thread.py +54 -0
- whatap/pack/__init__.py +0 -0
- whatap/pack/logSinkPack.py +77 -0
- whatap/pack/pack.py +34 -0
- whatap/pack/pack_enum.py +41 -0
- whatap/pack/tagCountPack.py +61 -0
- whatap/scripts/__init__.py +208 -0
- whatap/trace/__init__.py +12 -0
- whatap/trace/mod/__init__.py +0 -0
- whatap/trace/mod/amqp/__init__.py +0 -0
- whatap/trace/mod/amqp/kombu.py +122 -0
- whatap/trace/mod/amqp/pika.py +62 -0
- whatap/trace/mod/application/__init__.py +0 -0
- whatap/trace/mod/application/bottle.py +34 -0
- whatap/trace/mod/application/celery.py +81 -0
- whatap/trace/mod/application/cherrypy.py +30 -0
- whatap/trace/mod/application/django.py +287 -0
- whatap/trace/mod/application/django_asgi.py +266 -0
- whatap/trace/mod/application/django_py3.py +251 -0
- whatap/trace/mod/application/fastapi/__init__.py +31 -0
- whatap/trace/mod/application/fastapi/endpoint.py +73 -0
- whatap/trace/mod/application/fastapi/exception_log.py +63 -0
- whatap/trace/mod/application/fastapi/instrumentation.py +204 -0
- whatap/trace/mod/application/fastapi/scope.py +115 -0
- whatap/trace/mod/application/fastapi/transaction.py +67 -0
- whatap/trace/mod/application/flask.py +52 -0
- whatap/trace/mod/application/frappe.py +224 -0
- whatap/trace/mod/application/graphql.py +170 -0
- whatap/trace/mod/application/nameko.py +39 -0
- whatap/trace/mod/application/odoo.py +63 -0
- whatap/trace/mod/application/starlette.py +126 -0
- whatap/trace/mod/application/tornado.py +163 -0
- whatap/trace/mod/application/wsgi.py +195 -0
- whatap/trace/mod/database/__init__.py +0 -0
- whatap/trace/mod/database/cxoracle.py +49 -0
- whatap/trace/mod/database/mongo.py +169 -0
- whatap/trace/mod/database/mysql.py +80 -0
- whatap/trace/mod/database/neo4j.py +90 -0
- whatap/trace/mod/database/psycopg2.py +45 -0
- whatap/trace/mod/database/psycopg3.py +359 -0
- whatap/trace/mod/database/redis.py +122 -0
- whatap/trace/mod/database/sqlalchemy.py +213 -0
- whatap/trace/mod/database/sqlite3.py +130 -0
- whatap/trace/mod/database/util.py +630 -0
- whatap/trace/mod/email/__init__.py +0 -0
- whatap/trace/mod/email/smtp.py +78 -0
- whatap/trace/mod/httpc/__init__.py +0 -0
- whatap/trace/mod/httpc/django.py +31 -0
- whatap/trace/mod/httpc/httplib.py +70 -0
- whatap/trace/mod/httpc/httpx.py +62 -0
- whatap/trace/mod/httpc/requests.py +20 -0
- whatap/trace/mod/httpc/urllib3.py +27 -0
- whatap/trace/mod/httpc/util.py +388 -0
- whatap/trace/mod/logging.py +161 -0
- whatap/trace/mod/plugin.py +84 -0
- whatap/trace/mod/standalone/__init__.py +0 -0
- whatap/trace/mod/standalone/multiple.py +293 -0
- whatap/trace/mod/standalone/single.py +135 -0
- whatap/trace/simple_trace_context.py +18 -0
- whatap/trace/trace_context.py +212 -0
- whatap/trace/trace_context_manager.py +244 -0
- whatap/trace/trace_error.py +84 -0
- whatap/trace/trace_handler.py +89 -0
- whatap/trace/trace_import.py +91 -0
- whatap/trace/trace_module_definition.py +156 -0
- whatap/util/__init__.py +0 -0
- whatap/util/bit_util.py +49 -0
- whatap/util/cardinality/__init__.py +0 -0
- whatap/util/cardinality/hyperloglog.py +84 -0
- whatap/util/cardinality/murmurhash.py +20 -0
- whatap/util/cardinality/registerset.py +60 -0
- whatap/util/compare_util.py +19 -0
- whatap/util/date_util.py +55 -0
- whatap/util/debug_util.py +73 -0
- whatap/util/escape_literal_sql.py +233 -0
- whatap/util/frame_util.py +20 -0
- whatap/util/hash_util.py +103 -0
- whatap/util/hexa32.py +66 -0
- whatap/util/int_set.py +199 -0
- whatap/util/ip_util.py +63 -0
- whatap/util/keygen.py +11 -0
- whatap/util/linked_list.py +113 -0
- whatap/util/linked_map.py +359 -0
- whatap/util/metering_util.py +103 -0
- whatap/util/request_double_queue.py +68 -0
- whatap/util/request_queue.py +60 -0
- whatap/util/string_util.py +20 -0
- whatap/util/throttle_util.py +99 -0
- whatap/util/userid_util.py +134 -0
- whatap/value/__init__.py +1 -0
- whatap/value/blob_value.py +38 -0
- whatap/value/boolean_value.py +33 -0
- whatap/value/decimal_value.py +36 -0
- whatap/value/double_summary.py +86 -0
- whatap/value/double_value.py +33 -0
- whatap/value/float_array.py +42 -0
- whatap/value/float_value.py +34 -0
- whatap/value/int_array.py +42 -0
- whatap/value/ip4_value.py +50 -0
- whatap/value/list_value.py +105 -0
- whatap/value/long_array.py +44 -0
- whatap/value/long_summary.py +83 -0
- whatap/value/map_value.py +154 -0
- whatap/value/null_value.py +21 -0
- whatap/value/number_value.py +33 -0
- whatap/value/summary_value.py +39 -0
- whatap/value/text_array.py +58 -0
- whatap/value/text_hash_value.py +37 -0
- whatap/value/text_value.py +43 -0
- whatap/value/value.py +26 -0
- whatap/value/value_enum.py +80 -0
- whatap/whatap.conf +14 -0
- whatap_python-2.1.0.dist-info/METADATA +87 -0
- whatap_python-2.1.0.dist-info/RECORD +227 -0
- whatap_python-2.1.0.dist-info/WHEEL +5 -0
- whatap_python-2.1.0.dist-info/entry_points.txt +6 -0
- 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)
|