whatap-python 2.1.0__tar.gz → 2.1.1__tar.gz
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_python-2.1.0 → whatap_python-2.1.1}/PKG-INFO +1 -1
- whatap_python-2.1.1/tests/test_stream_ttft.py +192 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/agent/darwin/amd64/whatap_python +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/agent/darwin/arm64/whatap_python +0 -0
- whatap_python-2.1.1/whatap/build.py +4 -0
- whatap_python-2.1.1/whatap/llm/providers/anthropic/messages/messages.py +86 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/anthropic/messages/messages_extractor.py +5 -1
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/interceptor.py +55 -5
- whatap_python-2.1.1/whatap/llm/providers/openai/chat/chat.py +103 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/chat/chat_extractor.py +2 -0
- whatap_python-2.1.1/whatap/llm/providers/openai/completions/completions.py +86 -0
- whatap_python-2.1.1/whatap/llm/providers/openai/embeddings/embeddings.py +75 -0
- whatap_python-2.1.1/whatap/llm/providers/openai/responses/responses.py +92 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/responses/responses_extractor.py +3 -0
- whatap_python-2.1.1/whatap/llm/providers/stream_accumulator.py +139 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap_python.egg-info/PKG-INFO +1 -1
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap_python.egg-info/SOURCES.txt +1 -0
- whatap_python-2.1.0/whatap/build.py +0 -4
- whatap_python-2.1.0/whatap/llm/providers/anthropic/messages/messages.py +0 -70
- whatap_python-2.1.0/whatap/llm/providers/openai/chat/chat.py +0 -82
- whatap_python-2.1.0/whatap/llm/providers/openai/completions/completions.py +0 -70
- whatap_python-2.1.0/whatap/llm/providers/openai/embeddings/embeddings.py +0 -59
- whatap_python-2.1.0/whatap/llm/providers/openai/responses/responses.py +0 -70
- whatap_python-2.1.0/whatap/llm/providers/stream_accumulator.py +0 -73
- {whatap_python-2.1.0 → whatap_python-2.1.1}/README.md +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/pyproject.toml +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/setup.cfg +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/setup.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/LICENSE +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/README.rst +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/__main__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/agent/linux/amd64/whatap_python +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/agent/linux/arm64/whatap_python +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/agent/windows/whatap_python.exe +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/bootstrap/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/bootstrap/sitecustomize.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/conf/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/conf/configuration.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/conf/configure.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/conf/license.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/control/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/counter_manager.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/tasks/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/tasks/base_task.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/tasks/llm_evaluator_task.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/tasks/llm_log_sink_task.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/tasks/llm_stat_task.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/counter/tasks/openfiledescriptor.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/io/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/io/data_inputx.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/io/data_outputx.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/definitions.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/base.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/answer_relevance.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/combined_judge.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/factuality.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/hallucination.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/llm_judge.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/pii_leak.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/prompt_injection.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/toxicity.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/builtins/url_scan.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/registry.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/sampler.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/evaluators/scope.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/features.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_input_message.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_log_sink_pack.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_output_message.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_step_eval_status.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_step_status.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_system_message.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_tool_calls.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_tool_results.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/log_sink_packs/llm_tx_status.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/pricing.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/prompt_meta.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/anthropic/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/anthropic/messages/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/anthropic/messages/messages_context.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/chat/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/chat/chat_context.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/completions/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/completions/completions_context.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/completions/completions_extractor.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/content_parser.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/embeddings/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/embeddings/embeddings_context.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/embeddings/embeddings_extractor.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/responses/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/responses/responses_context.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/active_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/answer_relevance_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/api_status_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/base_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/combined_judge_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/error_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/factuality_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/feature_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/finish_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/hallucination_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/meter.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/perf_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/pii_leak_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/prompt_injection_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/token_usage_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/toxicity_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/stats/url_scan_eval_stat.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/async_sender.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/packet_enum.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/packet_type_enum.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/param_def.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/stackhelper.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/udp_session.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/net/udp_thread.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/pack/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/pack/logSinkPack.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/pack/pack.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/pack/pack_enum.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/pack/tagCountPack.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/scripts/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/amqp/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/amqp/kombu.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/amqp/pika.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/bottle.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/celery.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/cherrypy.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/django.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/django_asgi.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/django_py3.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/fastapi/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/fastapi/endpoint.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/fastapi/exception_log.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/fastapi/instrumentation.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/fastapi/scope.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/fastapi/transaction.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/flask.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/frappe.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/graphql.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/nameko.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/odoo.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/starlette.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/tornado.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/application/wsgi.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/cxoracle.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/mongo.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/mysql.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/neo4j.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/psycopg2.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/psycopg3.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/redis.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/sqlalchemy.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/sqlite3.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/database/util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/email/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/email/smtp.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/httpc/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/httpc/django.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/httpc/httplib.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/httpc/httpx.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/httpc/requests.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/httpc/urllib3.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/httpc/util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/logging.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/plugin.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/standalone/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/standalone/multiple.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/mod/standalone/single.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/simple_trace_context.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/trace_context.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/trace_context_manager.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/trace_error.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/trace_handler.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/trace_import.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/trace/trace_module_definition.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/bit_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/cardinality/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/cardinality/hyperloglog.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/cardinality/murmurhash.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/cardinality/registerset.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/compare_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/date_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/debug_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/escape_literal_sql.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/frame_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/hash_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/hexa32.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/int_set.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/ip_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/keygen.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/linked_list.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/linked_map.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/metering_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/request_double_queue.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/request_queue.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/string_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/throttle_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/util/userid_util.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/__init__.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/blob_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/boolean_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/decimal_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/double_summary.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/double_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/float_array.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/float_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/int_array.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/ip4_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/list_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/long_array.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/long_summary.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/map_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/null_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/number_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/summary_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/text_array.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/text_hash_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/text_value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/value.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/value/value_enum.py +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/whatap.conf +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap_python.egg-info/dependency_links.txt +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap_python.egg-info/entry_points.txt +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap_python.egg-info/not-zip-safe +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap_python.egg-info/requires.txt +0 -0
- {whatap_python-2.1.0 → whatap_python-2.1.1}/whatap_python.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""스트리밍 LLM 응답에서 TTFT/TPOT 트리거 회귀 테스트.
|
|
2
|
+
|
|
3
|
+
버그: tool_calls / reasoning 위주 스트리밍 응답에서 텍스트 content delta 가 없으면
|
|
4
|
+
on_first_token() 이 호출되지 않아 pack.ttft 가 None 이 되고, 그 결과 perf_stat 의
|
|
5
|
+
TPOT 도 계산되지 않았다. (latency / output_tokens 는 정상)
|
|
6
|
+
|
|
7
|
+
이 테스트는 stdlib unittest 로 작성됐고, provider SDK(openai/anthropic) 설치 없이
|
|
8
|
+
실행되도록 extractor 모듈을 파일 경로로 직접 로드한다(패키지 __init__ 의 SDK import 우회).
|
|
9
|
+
|
|
10
|
+
실행: python3 -m unittest tests.test_stream_ttft -v
|
|
11
|
+
"""
|
|
12
|
+
import importlib.util
|
|
13
|
+
import os
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
_BASE = os.path.join(
|
|
17
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
18
|
+
"whatap", "llm", "providers",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load(name, relpath):
|
|
23
|
+
spec = importlib.util.spec_from_file_location(name, os.path.join(_BASE, relpath))
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(module)
|
|
26
|
+
return module
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
chat_extractor = _load("chat_extractor_ut", "openai/chat/chat_extractor.py")
|
|
30
|
+
messages_extractor = _load("messages_extractor_ut", "anthropic/messages/messages_extractor.py")
|
|
31
|
+
responses_extractor = _load("responses_extractor_ut", "openai/responses/responses_extractor.py")
|
|
32
|
+
completions_extractor = _load("completions_extractor_ut", "openai/completions/completions_extractor.py")
|
|
33
|
+
|
|
34
|
+
from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
|
|
35
|
+
from whatap.llm.stats.perf_stat import PerfStat
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Obj(object):
|
|
39
|
+
"""getattr 기반 더미 — 키워드로 임의 속성을 갖는 객체를 만든다."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, **kw):
|
|
42
|
+
for k, v in kw.items():
|
|
43
|
+
setattr(self, k, v)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _chat_chunk(content=None, reasoning=None, tool_calls=None, usage=None, finish_reason=None):
|
|
47
|
+
delta = _Obj(content=content, reasoning_content=reasoning, reasoning=None, tool_calls=tool_calls)
|
|
48
|
+
choice = _Obj(delta=delta, finish_reason=finish_reason)
|
|
49
|
+
return _Obj(choices=[choice], usage=usage)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _tool_call_delta(index=0, call_id=None, name=None, arguments=None):
|
|
53
|
+
fn = _Obj(name=name, arguments=arguments)
|
|
54
|
+
return _Obj(index=index, id=call_id, function=fn)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ChatStreamTTFTTest(unittest.TestCase):
|
|
58
|
+
"""OpenAI Chat Completions 스트림의 on_first_token 트리거."""
|
|
59
|
+
|
|
60
|
+
def _new(self):
|
|
61
|
+
pack = LlmStepStatus()
|
|
62
|
+
return chat_extractor.ChatStream(pack, active_key=None)
|
|
63
|
+
|
|
64
|
+
def test_text_delta_sets_ttft(self):
|
|
65
|
+
"""회귀: 텍스트 content delta 는 여전히 첫 토큰으로 인식된다."""
|
|
66
|
+
acc = self._new()
|
|
67
|
+
acc.on_chunk(_chat_chunk(content="Hello"))
|
|
68
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
69
|
+
|
|
70
|
+
def test_tool_call_only_sets_ttft(self):
|
|
71
|
+
"""수정: content 없이 tool_calls delta 만 와도 첫 토큰으로 인식된다."""
|
|
72
|
+
acc = self._new()
|
|
73
|
+
acc.on_chunk(_chat_chunk(tool_calls=[_tool_call_delta(call_id="call_1", name="get_weather", arguments='{"c')]))
|
|
74
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
75
|
+
self.assertTrue(acc.has_tool)
|
|
76
|
+
|
|
77
|
+
def test_reasoning_only_sets_ttft(self):
|
|
78
|
+
"""수정: content 없이 reasoning delta 만 와도 첫 토큰으로 인식된다."""
|
|
79
|
+
acc = self._new()
|
|
80
|
+
acc.on_chunk(_chat_chunk(reasoning="Let me think"))
|
|
81
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
82
|
+
self.assertEqual(acc.reasoning, "Let me think")
|
|
83
|
+
|
|
84
|
+
def test_first_token_time_idempotent(self):
|
|
85
|
+
"""on_first_token 은 멱등 — 첫 delta 시각이 유지된다."""
|
|
86
|
+
acc = self._new()
|
|
87
|
+
acc.on_chunk(_chat_chunk(tool_calls=[_tool_call_delta(call_id="c", name="f", arguments="{}")]))
|
|
88
|
+
first = acc.first_token_time
|
|
89
|
+
acc.on_chunk(_chat_chunk(content="text"))
|
|
90
|
+
self.assertEqual(acc.first_token_time, first)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AnthropicStreamTTFTTest(unittest.TestCase):
|
|
94
|
+
"""Anthropic Messages 스트림의 on_first_token 트리거."""
|
|
95
|
+
|
|
96
|
+
def _new(self):
|
|
97
|
+
return messages_extractor.AnthropicStream(LlmStepStatus(), active_key=None, features=[])
|
|
98
|
+
|
|
99
|
+
def test_text_delta_sets_ttft(self):
|
|
100
|
+
acc = self._new()
|
|
101
|
+
acc.on_chunk(_Obj(type="content_block_start", content_block=_Obj(type="text", name=None)))
|
|
102
|
+
acc.on_chunk(_Obj(type="content_block_delta", delta=_Obj(text="Hi", thinking=None)))
|
|
103
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
104
|
+
|
|
105
|
+
def test_tool_use_block_sets_ttft(self):
|
|
106
|
+
"""수정: tool_use 블록 시작 시 첫 토큰으로 인식."""
|
|
107
|
+
acc = self._new()
|
|
108
|
+
acc.on_chunk(_Obj(type="content_block_start", content_block=_Obj(type="tool_use", name="get_weather")))
|
|
109
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
110
|
+
|
|
111
|
+
def test_thinking_delta_sets_ttft(self):
|
|
112
|
+
"""수정: thinking(reasoning) delta 시 첫 토큰으로 인식."""
|
|
113
|
+
acc = self._new()
|
|
114
|
+
acc.on_chunk(_Obj(type="content_block_start", content_block=_Obj(type="thinking", name=None)))
|
|
115
|
+
acc.on_chunk(_Obj(type="content_block_delta", delta=_Obj(thinking="hmm", text=None)))
|
|
116
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
117
|
+
self.assertEqual(acc.reasoning, "hmm")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ResponsesStreamTTFTTest(unittest.TestCase):
|
|
121
|
+
"""OpenAI Responses 스트림의 on_first_token 트리거."""
|
|
122
|
+
|
|
123
|
+
def _new(self):
|
|
124
|
+
return responses_extractor.ResponsesStream(LlmStepStatus(), active_key=None)
|
|
125
|
+
|
|
126
|
+
def test_text_delta_sets_ttft(self):
|
|
127
|
+
acc = self._new()
|
|
128
|
+
acc.on_chunk(_Obj(type="response.output_text.delta", delta="Hi"))
|
|
129
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
130
|
+
|
|
131
|
+
def test_function_call_item_sets_ttft(self):
|
|
132
|
+
"""수정: function_call 아이템 추가 시 첫 토큰으로 인식."""
|
|
133
|
+
acc = self._new()
|
|
134
|
+
acc.on_chunk(_Obj(type="response.output_item.added",
|
|
135
|
+
item=_Obj(type="function_call", call_id="call_1", id="i1", name="get_weather")))
|
|
136
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
137
|
+
|
|
138
|
+
def test_function_call_arguments_delta_sets_ttft(self):
|
|
139
|
+
"""수정: function_call 인자 delta 시에도 첫 토큰으로 인식(아이템 누락 대비)."""
|
|
140
|
+
acc = self._new()
|
|
141
|
+
acc.on_chunk(_Obj(type="response.output_item.added",
|
|
142
|
+
item=_Obj(type="function_call", call_id="call_1", id="i1", name="f")))
|
|
143
|
+
ft = acc.first_token_time
|
|
144
|
+
acc.on_chunk(_Obj(type="response.function_call_arguments.delta", call_id="call_1", delta='{"a":1}'))
|
|
145
|
+
self.assertEqual(acc.first_token_time, ft) # 멱등
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class CompletionsStreamTTFTTest(unittest.TestCase):
|
|
149
|
+
"""레거시 Completions 스트림(텍스트 전용) 회귀 확인."""
|
|
150
|
+
|
|
151
|
+
def test_text_delta_sets_ttft(self):
|
|
152
|
+
acc = completions_extractor.CompletionsStream(LlmStepStatus(), active_key=None)
|
|
153
|
+
acc.on_chunk(_Obj(choices=[_Obj(text="Hi", finish_reason=None)], usage=None))
|
|
154
|
+
self.assertIsNotNone(acc.first_token_time)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class PerfStatTPOTTest(unittest.TestCase):
|
|
158
|
+
"""ttft 가 세팅되면 perf_stat 이 TPOT 를 계산한다."""
|
|
159
|
+
|
|
160
|
+
def _pack(self, ttft, latency, output_tokens):
|
|
161
|
+
p = LlmStepStatus()
|
|
162
|
+
p.model = "gpt-5.4"
|
|
163
|
+
p.provider = "openai"
|
|
164
|
+
p.stream = True
|
|
165
|
+
p.success = True
|
|
166
|
+
p.ttft = ttft
|
|
167
|
+
p.latency = latency
|
|
168
|
+
p.output_tokens = output_tokens
|
|
169
|
+
return p
|
|
170
|
+
|
|
171
|
+
def test_tpot_recorded_when_ttft_present(self):
|
|
172
|
+
stat = PerfStat()
|
|
173
|
+
stat.update_from_pack(self._pack(ttft=100, latency=900, output_tokens=9))
|
|
174
|
+
# tpot = (900-100)/(9-1) = 100
|
|
175
|
+
keys = list(stat._stats["tpot_count"].keys())
|
|
176
|
+
self.assertEqual(len(keys), 1)
|
|
177
|
+
self.assertEqual(stat._stats["tpot_count"][keys[0]], 1)
|
|
178
|
+
self.assertAlmostEqual(stat._stats["tpot_sum"][keys[0]], 100.0)
|
|
179
|
+
self.assertEqual(stat._stats["ttft_count"][keys[0]], 1)
|
|
180
|
+
|
|
181
|
+
def test_tpot_absent_when_ttft_none(self):
|
|
182
|
+
"""버그 재현: ttft 가 None 이면 TPOT 미기록(분자 계산 불가)."""
|
|
183
|
+
stat = PerfStat()
|
|
184
|
+
stat.update_from_pack(self._pack(ttft=None, latency=900, output_tokens=9))
|
|
185
|
+
key = list(stat._stats["call_count"].keys())[0] # 호출은 기록되지만
|
|
186
|
+
self.assertEqual(stat._stats["call_count"][key], 1)
|
|
187
|
+
self.assertEqual(stat._stats["tpot_count"][key], 0) # TPOT/TTFT 는 미기록
|
|
188
|
+
self.assertEqual(stat._stats["ttft_count"][key], 0)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
unittest.main()
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,86 @@
|
|
|
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, extract_response, _safe, _clear_httpc_pending,
|
|
7
|
+
)
|
|
8
|
+
from whatap.llm.providers.stream_accumulator import wrap_sync_stream, wrap_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 = active_key = None
|
|
16
|
+
try:
|
|
17
|
+
pack, ctx, features, stream = build_context(kwargs)
|
|
18
|
+
capture_client(pack, ctx, args)
|
|
19
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
20
|
+
before_call(pack, active_key)
|
|
21
|
+
except Exception:
|
|
22
|
+
if pack is not None and active_key is not None:
|
|
23
|
+
_safe(_ensure_end, pack, active_key)
|
|
24
|
+
return fn(*args, **kwargs)
|
|
25
|
+
|
|
26
|
+
_stream_returned = False
|
|
27
|
+
try:
|
|
28
|
+
try:
|
|
29
|
+
response = fn(*args, **kwargs)
|
|
30
|
+
except Exception as err:
|
|
31
|
+
_safe(handle_error, pack, err, active_key, APIError)
|
|
32
|
+
raise
|
|
33
|
+
finally:
|
|
34
|
+
_safe(_clear_httpc_pending, ctx)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
after_call(pack, ctx)
|
|
38
|
+
if stream:
|
|
39
|
+
result, _stream_returned = wrap_sync_stream(response, AnthropicStream(pack, active_key, features))
|
|
40
|
+
return result
|
|
41
|
+
extract_response(response, finalize, pack, features)
|
|
42
|
+
finalize_non_streaming(pack, active_key)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
return response
|
|
46
|
+
finally:
|
|
47
|
+
if not _stream_returned:
|
|
48
|
+
_safe(_ensure_end, pack, active_key)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def async_intercept_create(fn, *args, **kwargs):
|
|
52
|
+
"""Anthropic Messages 비동기 호출을 인터셉트한다. 계측 실패는 사용자 호출로 전파되지 않는다."""
|
|
53
|
+
pack = active_key = None
|
|
54
|
+
try:
|
|
55
|
+
pack, ctx, features, stream = build_context(kwargs)
|
|
56
|
+
capture_client(pack, ctx, args)
|
|
57
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
58
|
+
before_call(pack, active_key)
|
|
59
|
+
except Exception:
|
|
60
|
+
if pack is not None and active_key is not None:
|
|
61
|
+
_safe(_ensure_end, pack, active_key)
|
|
62
|
+
return await fn(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
_stream_returned = False
|
|
65
|
+
try:
|
|
66
|
+
try:
|
|
67
|
+
response = await fn(*args, **kwargs)
|
|
68
|
+
except Exception as err:
|
|
69
|
+
_safe(handle_error, pack, err, active_key, APIError)
|
|
70
|
+
raise
|
|
71
|
+
finally:
|
|
72
|
+
_safe(_clear_httpc_pending, ctx)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
after_call(pack, ctx)
|
|
76
|
+
if stream:
|
|
77
|
+
result, _stream_returned = wrap_async_stream(response, AnthropicStream(pack, active_key, features))
|
|
78
|
+
return result
|
|
79
|
+
extract_response(response, finalize, pack, features)
|
|
80
|
+
finalize_non_streaming(pack, active_key)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
return response
|
|
84
|
+
finally:
|
|
85
|
+
if not _stream_returned:
|
|
86
|
+
_safe(_ensure_end, pack, active_key)
|
|
@@ -84,6 +84,7 @@ class AnthropicStream(StreamAccumulator):
|
|
|
84
84
|
self.block_type = getattr(block, 'type', None)
|
|
85
85
|
self.block_name = getattr(block, 'name', None)
|
|
86
86
|
if self.block_type == 'tool_use':
|
|
87
|
+
self.on_first_token()
|
|
87
88
|
tag = (LlmFeature.COMPUTER_USE
|
|
88
89
|
if self.block_name and 'computer' in self.block_name
|
|
89
90
|
else LlmFeature.TOOL_USE)
|
|
@@ -92,7 +93,10 @@ class AnthropicStream(StreamAccumulator):
|
|
|
92
93
|
elif t == 'content_block_delta':
|
|
93
94
|
delta = getattr(event, 'delta', None)
|
|
94
95
|
if self.block_type == 'thinking':
|
|
95
|
-
|
|
96
|
+
thinking = getattr(delta, 'thinking', '') or ''
|
|
97
|
+
if thinking:
|
|
98
|
+
self.on_first_token()
|
|
99
|
+
self.reasoning += thinking
|
|
96
100
|
elif self.block_type == 'text':
|
|
97
101
|
text = getattr(delta, 'text', '') or ''
|
|
98
102
|
if text:
|
|
@@ -6,6 +6,7 @@ API 호출 전후 처리 흐름:
|
|
|
6
6
|
"""
|
|
7
7
|
import time
|
|
8
8
|
|
|
9
|
+
from whatap import logging
|
|
9
10
|
from whatap.counter.tasks.llm_log_sink_task import dispatch_llm_pack
|
|
10
11
|
|
|
11
12
|
|
|
@@ -93,6 +94,25 @@ def _active_stat():
|
|
|
93
94
|
return LlmStatTask.get_stat('ActiveStat')
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
def _safe(fn, *args, **kwargs):
|
|
98
|
+
"""계측 보조 단계를 안전하게 실행한다 — 예외를 흡수(디버그 로깅)해 사용자 호출을 보호.
|
|
99
|
+
|
|
100
|
+
계측은 어떤 경우에도 사용자 애플리케이션을 깨면 안 된다. 인터셉트 라이프사이클의
|
|
101
|
+
모든 보조 호출(before/after/handle_error/_ensure_end 등)을 이걸로 감싼다.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
return fn(*args, **kwargs)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logging.debug('[LLM] instrumentation step skipped: %s' % e, extra={'id': 'LLM008'})
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _clear_httpc_pending(ctx):
|
|
111
|
+
"""fn() 직후 httpc pending 플래그 해제(있을 때만)."""
|
|
112
|
+
if ctx is not None:
|
|
113
|
+
ctx._llm_httpc_pending = False
|
|
114
|
+
|
|
115
|
+
|
|
96
116
|
def before_call(pack, active_key):
|
|
97
117
|
"""API 호출 전: active 카운터 증가 + 시작 시간 기록 + 순차 인덱스 할당."""
|
|
98
118
|
pack._active_ended = False
|
|
@@ -174,9 +194,39 @@ def finalize_non_streaming(pack, active_key):
|
|
|
174
194
|
_ensure_end(pack, active_key)
|
|
175
195
|
|
|
176
196
|
|
|
197
|
+
def extract_response(response, finalize_fn, pack, *finalize_args):
|
|
198
|
+
"""비스트리밍 응답을 계측한다. 어떤 예외도 사용자 호출로 전파시키지 않는다.
|
|
199
|
+
|
|
200
|
+
계측은 사용자 애플리케이션을 절대 깨면 안 된다. finalize_fn 은 응답 구조
|
|
201
|
+
(``.choices`` / ``.content`` / ``.output`` 등) 를 단정하므로, 예상 밖 응답
|
|
202
|
+
(예: litellm/langchain 이 ``with_raw_response`` 로 받는 ``LegacyAPIResponse``)
|
|
203
|
+
이 와도 여기서 흡수하고 계측만 생략한다.
|
|
204
|
+
|
|
205
|
+
또한 응답이 ``parse()`` 를 가진 raw 래퍼면 parse() 로 실제 응답을 꺼내 계측한다.
|
|
206
|
+
parse() 결과는 캐시되어 호출측(litellm 등)의 후속 parse() 와 공유된다.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
target = response
|
|
210
|
+
parse = getattr(response, "parse", None)
|
|
211
|
+
if callable(parse):
|
|
212
|
+
try:
|
|
213
|
+
target = parse()
|
|
214
|
+
except Exception:
|
|
215
|
+
target = response
|
|
216
|
+
finalize_fn(target, pack, *finalize_args)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logging.debug('[LLM] response extract skipped: %s' % e, extra={'id': 'LLM005'})
|
|
219
|
+
|
|
220
|
+
|
|
177
221
|
def _dispatch(pack):
|
|
178
|
-
"""로그싱크팩 전송 + 메트릭 stat 업데이트 통합 호출."""
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
222
|
+
"""로그싱크팩 전송 + 메트릭 stat 업데이트 통합 호출. 송출 실패는 사용자에게 전파 안 함."""
|
|
223
|
+
try:
|
|
224
|
+
dispatch_llm_pack(pack)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logging.debug('[LLM] dispatch failed: %s' % e, extra={'id': 'LLM006'})
|
|
227
|
+
try:
|
|
228
|
+
inst = _stat_task()
|
|
229
|
+
if inst:
|
|
230
|
+
inst.notify(pack)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logging.debug('[LLM] stat notify failed: %s' % e, extra={'id': 'LLM007'})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""OpenAI Chat 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, extract_response, _safe, _clear_httpc_pending,
|
|
7
|
+
)
|
|
8
|
+
from whatap.llm.providers.stream_accumulator import wrap_sync_stream, wrap_async_stream
|
|
9
|
+
from whatap.llm.providers.openai.chat.chat_context import build_context
|
|
10
|
+
from whatap.llm.providers.openai.chat.chat_extractor import finalize, ChatStream
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def intercept_create(fn, *args, **kwargs):
|
|
14
|
+
"""OpenAI Chat Completions 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다.
|
|
15
|
+
|
|
16
|
+
계측 어느 단계가 실패해도 사용자 호출은 보호된다 — 계측 예외는 전파하지 않고
|
|
17
|
+
사용자 fn 의 예외만 전파한다.
|
|
18
|
+
"""
|
|
19
|
+
pack = active_key = None
|
|
20
|
+
try:
|
|
21
|
+
pack, ctx, features, stream = build_context(kwargs)
|
|
22
|
+
capture_client(pack, ctx, args)
|
|
23
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
24
|
+
if stream:
|
|
25
|
+
opts = dict(kwargs.get("stream_options") or {})
|
|
26
|
+
if not opts.get("include_usage"):
|
|
27
|
+
opts["include_usage"] = True
|
|
28
|
+
kwargs["stream_options"] = opts
|
|
29
|
+
before_call(pack, active_key)
|
|
30
|
+
except Exception:
|
|
31
|
+
if pack is not None and active_key is not None:
|
|
32
|
+
_safe(_ensure_end, pack, active_key)
|
|
33
|
+
return fn(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
_stream_returned = False
|
|
36
|
+
try:
|
|
37
|
+
try:
|
|
38
|
+
response = fn(*args, **kwargs)
|
|
39
|
+
except Exception as err:
|
|
40
|
+
_safe(handle_error, pack, err, active_key, OpenAIError)
|
|
41
|
+
raise
|
|
42
|
+
finally:
|
|
43
|
+
_safe(_clear_httpc_pending, ctx)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
after_call(pack, ctx)
|
|
47
|
+
if stream:
|
|
48
|
+
result, _stream_returned = wrap_sync_stream(response, ChatStream(pack, active_key))
|
|
49
|
+
return result
|
|
50
|
+
extract_response(response, finalize, pack, features)
|
|
51
|
+
finalize_non_streaming(pack, active_key)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return response
|
|
55
|
+
finally:
|
|
56
|
+
if not _stream_returned:
|
|
57
|
+
_safe(_ensure_end, pack, active_key)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def intercept_create_async(fn, *args, **kwargs):
|
|
61
|
+
"""OpenAI Chat Completions 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다.
|
|
62
|
+
|
|
63
|
+
계측 어느 단계가 실패해도 사용자 호출은 보호된다.
|
|
64
|
+
"""
|
|
65
|
+
pack = active_key = None
|
|
66
|
+
try:
|
|
67
|
+
pack, ctx, features, stream = build_context(kwargs)
|
|
68
|
+
capture_client(pack, ctx, args)
|
|
69
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
70
|
+
if stream:
|
|
71
|
+
opts = dict(kwargs.get("stream_options") or {})
|
|
72
|
+
if not opts.get("include_usage"):
|
|
73
|
+
opts["include_usage"] = True
|
|
74
|
+
kwargs["stream_options"] = opts
|
|
75
|
+
before_call(pack, active_key)
|
|
76
|
+
except Exception:
|
|
77
|
+
if pack is not None and active_key is not None:
|
|
78
|
+
_safe(_ensure_end, pack, active_key)
|
|
79
|
+
return await fn(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
_stream_returned = False
|
|
82
|
+
try:
|
|
83
|
+
try:
|
|
84
|
+
response = await fn(*args, **kwargs)
|
|
85
|
+
except Exception as err:
|
|
86
|
+
_safe(handle_error, pack, err, active_key, OpenAIError)
|
|
87
|
+
raise
|
|
88
|
+
finally:
|
|
89
|
+
_safe(_clear_httpc_pending, ctx)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
after_call(pack, ctx)
|
|
93
|
+
if stream:
|
|
94
|
+
result, _stream_returned = wrap_async_stream(response, ChatStream(pack, active_key))
|
|
95
|
+
return result
|
|
96
|
+
extract_response(response, finalize, pack, features)
|
|
97
|
+
finalize_non_streaming(pack, active_key)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
return response
|
|
101
|
+
finally:
|
|
102
|
+
if not _stream_returned:
|
|
103
|
+
_safe(_ensure_end, pack, active_key)
|
{whatap_python-2.1.0 → whatap_python-2.1.1}/whatap/llm/providers/openai/chat/chat_extractor.py
RENAMED
|
@@ -89,9 +89,11 @@ class ChatStream(StreamAccumulator):
|
|
|
89
89
|
getattr(delta, "reasoning_content", None) or
|
|
90
90
|
getattr(delta, "reasoning", None) or "")
|
|
91
91
|
if reasoning:
|
|
92
|
+
self.on_first_token()
|
|
92
93
|
self.reasoning += reasoning
|
|
93
94
|
|
|
94
95
|
if getattr(delta, "tool_calls", None):
|
|
96
|
+
self.on_first_token()
|
|
95
97
|
self.has_tool = True
|
|
96
98
|
for tc in delta.tool_calls:
|
|
97
99
|
idx = tc.index
|
|
@@ -0,0 +1,86 @@
|
|
|
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, extract_response, _safe, _clear_httpc_pending,
|
|
7
|
+
)
|
|
8
|
+
from whatap.llm.providers.stream_accumulator import wrap_sync_stream, wrap_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 = active_key = None
|
|
16
|
+
try:
|
|
17
|
+
pack, ctx = build_context(kwargs)
|
|
18
|
+
capture_client(pack, ctx, args)
|
|
19
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
20
|
+
before_call(pack, active_key)
|
|
21
|
+
except Exception:
|
|
22
|
+
if pack is not None and active_key is not None:
|
|
23
|
+
_safe(_ensure_end, pack, active_key)
|
|
24
|
+
return fn(*args, **kwargs)
|
|
25
|
+
|
|
26
|
+
_stream_returned = False
|
|
27
|
+
try:
|
|
28
|
+
try:
|
|
29
|
+
response = fn(*args, **kwargs)
|
|
30
|
+
except Exception as err:
|
|
31
|
+
_safe(handle_error, pack, err, active_key, OpenAIError)
|
|
32
|
+
raise
|
|
33
|
+
finally:
|
|
34
|
+
_safe(_clear_httpc_pending, ctx)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
after_call(pack, ctx)
|
|
38
|
+
if pack.stream:
|
|
39
|
+
result, _stream_returned = wrap_sync_stream(response, CompletionsStream(pack, active_key))
|
|
40
|
+
return result
|
|
41
|
+
extract_response(response, finalize, pack)
|
|
42
|
+
finalize_non_streaming(pack, active_key)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
return response
|
|
46
|
+
finally:
|
|
47
|
+
if not _stream_returned:
|
|
48
|
+
_safe(_ensure_end, pack, active_key)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def intercept_completions_async(fn, *args, **kwargs):
|
|
52
|
+
"""Completions API 비동기 인터셉트. 계측 실패는 사용자 호출로 전파되지 않는다."""
|
|
53
|
+
pack = active_key = None
|
|
54
|
+
try:
|
|
55
|
+
pack, ctx = build_context(kwargs)
|
|
56
|
+
capture_client(pack, ctx, args)
|
|
57
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
58
|
+
before_call(pack, active_key)
|
|
59
|
+
except Exception:
|
|
60
|
+
if pack is not None and active_key is not None:
|
|
61
|
+
_safe(_ensure_end, pack, active_key)
|
|
62
|
+
return await fn(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
_stream_returned = False
|
|
65
|
+
try:
|
|
66
|
+
try:
|
|
67
|
+
response = await fn(*args, **kwargs)
|
|
68
|
+
except Exception as err:
|
|
69
|
+
_safe(handle_error, pack, err, active_key, OpenAIError)
|
|
70
|
+
raise
|
|
71
|
+
finally:
|
|
72
|
+
_safe(_clear_httpc_pending, ctx)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
after_call(pack, ctx)
|
|
76
|
+
if pack.stream:
|
|
77
|
+
result, _stream_returned = wrap_async_stream(response, CompletionsStream(pack, active_key))
|
|
78
|
+
return result
|
|
79
|
+
extract_response(response, finalize, pack)
|
|
80
|
+
finalize_non_streaming(pack, active_key)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
return response
|
|
84
|
+
finally:
|
|
85
|
+
if not _stream_returned:
|
|
86
|
+
_safe(_ensure_end, pack, active_key)
|
|
@@ -0,0 +1,75 @@
|
|
|
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, extract_response, _safe, _clear_httpc_pending,
|
|
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 = active_key = None
|
|
15
|
+
try:
|
|
16
|
+
pack, ctx = build_context(kwargs)
|
|
17
|
+
capture_client(pack, ctx, args)
|
|
18
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
19
|
+
before_call(pack, active_key)
|
|
20
|
+
except Exception:
|
|
21
|
+
if pack is not None and active_key is not None:
|
|
22
|
+
_safe(_ensure_end, pack, active_key)
|
|
23
|
+
return fn(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
try:
|
|
27
|
+
response = fn(*args, **kwargs)
|
|
28
|
+
except Exception as err:
|
|
29
|
+
_safe(handle_error, pack, err, active_key, OpenAIError)
|
|
30
|
+
raise
|
|
31
|
+
finally:
|
|
32
|
+
_safe(_clear_httpc_pending, ctx)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
after_call(pack, ctx)
|
|
36
|
+
extract_response(response, finalize, pack, kwargs)
|
|
37
|
+
finalize_non_streaming(pack, active_key)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
return response
|
|
41
|
+
finally:
|
|
42
|
+
_safe(_ensure_end, pack, active_key)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def intercept_embeddings_async(fn, *args, **kwargs):
|
|
46
|
+
"""OpenAI Embeddings 비동기 호출을 인터셉트한다. 계측 실패는 사용자 호출로 전파되지 않는다."""
|
|
47
|
+
pack = active_key = None
|
|
48
|
+
try:
|
|
49
|
+
pack, ctx = build_context(kwargs)
|
|
50
|
+
capture_client(pack, ctx, args)
|
|
51
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
52
|
+
before_call(pack, active_key)
|
|
53
|
+
except Exception:
|
|
54
|
+
if pack is not None and active_key is not None:
|
|
55
|
+
_safe(_ensure_end, pack, active_key)
|
|
56
|
+
return await fn(*args, **kwargs)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
try:
|
|
60
|
+
response = await fn(*args, **kwargs)
|
|
61
|
+
except Exception as err:
|
|
62
|
+
_safe(handle_error, pack, err, active_key, OpenAIError)
|
|
63
|
+
raise
|
|
64
|
+
finally:
|
|
65
|
+
_safe(_clear_httpc_pending, ctx)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
after_call(pack, ctx)
|
|
69
|
+
extract_response(response, finalize, pack, kwargs)
|
|
70
|
+
finalize_non_streaming(pack, active_key)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return response
|
|
74
|
+
finally:
|
|
75
|
+
_safe(_ensure_end, pack, active_key)
|