diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py index 395e6a98ac..9897884bbb 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py @@ -17,6 +17,7 @@ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.metrics import get_meter from opentelemetry.semconv_ai import Meters, SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY +from opentelemetry.semconv._incubating.metrics import gen_ai_metrics as GenAIMetrics from opentelemetry.trace import get_tracer from opentelemetry.trace.propagation import set_span_in_context from opentelemetry.trace.propagation.tracecontext import ( @@ -68,6 +69,34 @@ def _instrument(self, **kwargs): description="Measures number of input and output tokens used", ) + # Create streaming time to first token histogram + ttft_histogram = meter.create_histogram( + name=GenAIMetrics.GEN_AI_SERVER_TIME_TO_FIRST_TOKEN, + unit="s", + description="Time to first token in streaming responses", + ) + + # Create streaming time to generate histogram + streaming_time_histogram = meter.create_histogram( + name=Meters.LLM_STREAMING_TIME_TO_GENERATE, + unit="s", + description="Time between first token and completion in streaming responses", + ) + + # Create generation choices counter + choices_counter = meter.create_counter( + name=Meters.LLM_GENERATION_CHOICES, + unit="choice", + description="Number of choices returned by completions call", + ) + + # Create exception counter + exception_counter = meter.create_counter( + name="llm.langchain.completions.exceptions", + unit="time", + description="Number of exceptions occurred during LangChain completions", + ) + if not Config.use_legacy_attributes: event_logger_provider = kwargs.get("event_logger_provider") Config.event_logger = get_event_logger( @@ -75,7 +104,8 @@ def _instrument(self, **kwargs): ) traceloopCallbackHandler = TraceloopCallbackHandler( - tracer, duration_histogram, token_histogram + tracer, duration_histogram, token_histogram, ttft_histogram, + streaming_time_histogram, choices_counter, exception_counter ) wrap_function_wrapper( module="langchain_core.callbacks", diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py index c50a0cff6c..66229b59a6 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py @@ -54,7 +54,7 @@ should_send_prompts, ) from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY -from opentelemetry.metrics import Histogram +from opentelemetry.metrics import Histogram, Counter from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_RESPONSE_ID, ) @@ -146,16 +146,65 @@ def _extract_tool_call_data( class TraceloopCallbackHandler(BaseCallbackHandler): def __init__( - self, tracer: Tracer, duration_histogram: Histogram, token_histogram: Histogram + self, + tracer: Tracer, + duration_histogram: Histogram, + token_histogram: Histogram, + ttft_histogram: Histogram, + streaming_time_histogram: Histogram, + choices_counter: Counter, + exception_counter: Counter ) -> None: super().__init__() self.tracer = tracer self.duration_histogram = duration_histogram self.token_histogram = token_histogram + self.ttft_histogram = ttft_histogram + self.streaming_time_histogram = streaming_time_histogram + self.choices_counter = choices_counter + self.exception_counter = exception_counter self.spans: dict[UUID, SpanHolder] = {} self.run_inline = True self._callback_manager: CallbackManager | AsyncCallbackManager = None + def _create_shared_attributes( + self, span, model_name: str, operation_type: str = None, is_streaming: bool = False + ) -> dict: + """Create shared attributes for metrics matching OpenAI SDK structure.""" + vendor = span.attributes.get(SpanAttributes.LLM_SYSTEM, "Langchain") + attributes = { + SpanAttributes.LLM_SYSTEM: vendor, + SpanAttributes.LLM_RESPONSE_MODEL: model_name, + } + # Add operation name if available + if operation_type: + attributes["gen_ai.operation.name"] = operation_type + elif span.attributes.get(SpanAttributes.LLM_REQUEST_TYPE): + attributes["gen_ai.operation.name"] = span.attributes.get(SpanAttributes.LLM_REQUEST_TYPE) + server_address = None + try: + association_properties = context_api.get_value("association_properties") or {} + server_address = ( + association_properties.get("api_base") or + association_properties.get("endpoint") or + association_properties.get("base_url") or + association_properties.get("server_address") + ) + except (AttributeError, KeyError, TypeError): + pass + + if not server_address: + # Check if we can get it from span attributes + server_address = span.attributes.get("server.address") + + if server_address: + attributes["server.address"] = server_address + + if is_streaming: + attributes["stream"] = True + + return attributes + @staticmethod def _get_name_from_callback( serialized: dict[str, Any], @@ -494,7 +543,7 @@ def on_chat_model_start( metadata=metadata, serialized=serialized, ) - set_request_params(span, kwargs, self.spans[run_id]) + set_request_params(span, kwargs, self.spans[run_id], serialized, metadata) if should_emit_events(): self._emit_chat_input_events(messages) else: @@ -524,13 +573,53 @@ def on_llm_start( LLMRequestTypeValues.COMPLETION, serialized=serialized, ) - set_request_params(span, kwargs, self.spans[run_id]) + set_request_params(span, kwargs, self.spans[run_id], serialized, metadata) if should_emit_events(): for prompt in prompts: emit_event(MessageEvent(content=prompt, role="user")) else: set_llm_request(span, serialized, prompts, kwargs, self.spans[run_id]) + @dont_throw + def on_llm_new_token( + self, + token: str, + *, + chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + **kwargs: Any, + ) -> Any: + """Run on new LLM token. Track TTFT and streaming metrics.""" + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + return + + if run_id not in self.spans: + return + + span_holder = self.spans[run_id] + current_time = time.time() + + # Track time to first token + if span_holder.first_token_time is None: + span_holder.first_token_time = current_time + ttft = current_time - span_holder.start_time + + # Record TTFT metric + span = span_holder.span + from opentelemetry.instrumentation.langchain.span_utils import _get_unified_unknown_model + + model_name = ( + span.attributes.get(SpanAttributes.LLM_RESPONSE_MODEL) or + span_holder.request_model or + _get_unified_unknown_model(existing_model=span_holder.request_model) + ) + + self.ttft_histogram.record( + ttft, + attributes=self._create_shared_attributes(span, model_name, is_streaming=True) + ) + @dont_throw def on_llm_end( self, @@ -552,7 +641,7 @@ def on_llm_end( ) or response.llm_output.get("model_id") if model_name is not None: _set_span_attribute( - span, SpanAttributes.LLM_RESPONSE_MODEL, model_name or "unknown" + span, SpanAttributes.LLM_RESPONSE_MODEL, model_name ) if self.spans[run_id].request_model is None: @@ -571,6 +660,20 @@ def on_llm_end( model_name = _extract_model_name_from_association_metadata( association_properties ) + + # Final fallback: use model name from request if all else fails + if model_name is None and run_id in self.spans and self.spans[run_id].request_model: + model_name = self.spans[run_id].request_model + + # Ensure model_name is never None for downstream usage + if model_name is None: + from opentelemetry.instrumentation.langchain.span_utils import _get_unified_unknown_model + existing_model = self.spans[run_id].request_model if run_id in self.spans else None + model_name = _get_unified_unknown_model(existing_model=existing_model) + + # Update span attribute with final resolved model name + _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, model_name) + token_usage = (response.llm_output or {}).get("token_usage") or ( response.llm_output or {} ).get("usage") @@ -600,26 +703,17 @@ def on_llm_end( ) # Record token usage metrics - vendor = span.attributes.get(SpanAttributes.LLM_SYSTEM, "Langchain") + base_attrs = self._create_shared_attributes(span, model_name) + if prompt_tokens > 0: - self.token_histogram.record( - prompt_tokens, - attributes={ - SpanAttributes.LLM_SYSTEM: vendor, - SpanAttributes.LLM_TOKEN_TYPE: "input", - SpanAttributes.LLM_RESPONSE_MODEL: model_name or "unknown", - }, - ) + input_attrs = {**base_attrs, SpanAttributes.LLM_TOKEN_TYPE: "input"} + self.token_histogram.record(prompt_tokens, attributes=input_attrs) if completion_tokens > 0: - self.token_histogram.record( - completion_tokens, - attributes={ - SpanAttributes.LLM_SYSTEM: vendor, - SpanAttributes.LLM_TOKEN_TYPE: "output", - SpanAttributes.LLM_RESPONSE_MODEL: model_name or "unknown", - }, - ) + output_attrs = {**base_attrs, SpanAttributes.LLM_TOKEN_TYPE: "output"} + self.token_histogram.record(completion_tokens, attributes=output_attrs) + # Always call set_chat_response_usage for complete usage metadata extraction + # The function handles duplicate recording prevention internally set_chat_response_usage( span, response, self.token_histogram, token_usage is None, model_name ) @@ -628,16 +722,27 @@ def on_llm_end( else: set_chat_response(span, response) - # Record duration before ending span - duration = time.time() - self.spans[run_id].start_time - vendor = span.attributes.get(SpanAttributes.LLM_SYSTEM, "Langchain") - self.duration_histogram.record( - duration, - attributes={ - SpanAttributes.LLM_SYSTEM: vendor, - SpanAttributes.LLM_RESPONSE_MODEL: model_name or "unknown", - }, - ) + # Record generation choices count + total_choices = 0 + for generation_list in response.generations: + total_choices += len(generation_list) + + span_holder = self.spans[run_id] + current_time = time.time() + is_streaming_request = span_holder.first_token_time is not None + + shared_attrs = self._create_shared_attributes(span, model_name, is_streaming=is_streaming_request) + + if total_choices > 0: + self.choices_counter.add(total_choices, attributes=shared_attrs) + + # Record streaming time to generate if TTFT was tracked + if span_holder.first_token_time is not None: + streaming_time = current_time - span_holder.first_token_time + self.streaming_time_histogram.record(streaming_time, attributes=shared_attrs) + + duration = current_time - span_holder.start_time + self.duration_histogram.record(duration, attributes=shared_attrs) self._end_span(span, run_id) @@ -752,6 +857,23 @@ def _handle_error( span = self._get_span(run_id) span.set_status(Status(StatusCode.ERROR)) span.record_exception(error) + + # Record exception metric for LLM errors + if run_id in self.spans: + span_holder = self.spans[run_id] + from opentelemetry.instrumentation.langchain.span_utils import _get_unified_unknown_model + + model_name = ( + span.attributes.get(SpanAttributes.LLM_RESPONSE_MODEL) or + span_holder.request_model or + _get_unified_unknown_model(existing_model=span_holder.request_model) + ) + + exception_attrs = self._create_shared_attributes(span, model_name) + exception_attrs[ERROR_TYPE] = type(error).__name__ + + self.exception_counter.add(1, attributes=exception_attrs) + self._end_span(span, run_id) @dont_throw diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py index 5469aad680..859bbfa171 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py @@ -34,6 +34,7 @@ class SpanHolder: entity_path: str start_time: float = field(default_factory=time.time) request_model: Optional[str] = None + first_token_time: Optional[float] = None def _message_type_to_role(message_type: str) -> str: @@ -54,21 +55,133 @@ def _set_span_attribute(span: Span, name: str, value: AttributeValue): span.set_attribute(name, value) -def set_request_params(span, kwargs, span_holder: SpanHolder): - if not span.is_recording(): - return +def _get_unified_unknown_model( + class_name: Optional[str] = None, existing_model: Optional[str] = None +) -> str: + """Get unified unknown model name to ensure consistency across all fallbacks.""" + + if existing_model: + existing_lower = existing_model.lower() + if existing_lower.startswith("deepseek"): + return "deepseek-unknown" + elif existing_lower.startswith("gpt"): + return "gpt-unknown" + elif existing_lower.startswith("claude"): + return "claude-unknown" + elif existing_lower.startswith("command"): + return "command-unknown" + elif ("ollama" in existing_lower or "llama" in existing_lower): + return "ollama-unknown" + + # Fallback to class name-based inference + if class_name: + if "ChatDeepSeek" in class_name: + return "deepseek-unknown" + elif "ChatOpenAI" in class_name: + return "gpt-unknown" + elif "ChatAnthropic" in class_name: + return "claude-unknown" + elif "ChatCohere" in class_name: + return "command-unknown" + elif "ChatOllama" in class_name: + return "ollama-unknown" + + return "unknown" + + +def _extract_model_name_from_request( + kwargs, span_holder: SpanHolder, serialized: Optional[dict] = None, metadata: Optional[dict] = None +) -> str: + """Enhanced model extraction supporting third-party LangChain integrations.""" for model_tag in ("model", "model_id", "model_name"): if (model := kwargs.get(model_tag)) is not None: - span_holder.request_model = model - break + return model elif ( model := (kwargs.get("invocation_params") or {}).get(model_tag) ) is not None: - span_holder.request_model = model - break - else: - model = "unknown" + return model + + # Enhanced extraction for third-party models + # Check nested kwargs structures + if "kwargs" in kwargs: + nested_kwargs = kwargs["kwargs"] + for model_tag in ("model", "model_id", "model_name"): + if (model := nested_kwargs.get(model_tag)) is not None: + return model + + # Try to extract from model configuration passed through kwargs + if "model_kwargs" in kwargs: + model_kwargs = kwargs["model_kwargs"] + for model_tag in ("model", "model_id", "model_name"): + if (model := model_kwargs.get(model_tag)) is not None: + return model + + # Check association metadata which is important for ChatDeepSeek and similar integrations + if metadata: + if (model := metadata.get("ls_model_name")) is not None: + return model + # Check other potential metadata fields + for model_tag in ("model", "model_id", "model_name"): + if (model := metadata.get(model_tag)) is not None: + return model + + # Try to get association properties from context + try: + from opentelemetry import context as context_api + association_properties = context_api.get_value("association_properties") or {} + if (model := association_properties.get("ls_model_name")) is not None: + return model + except (AttributeError, KeyError, TypeError): + pass + + # Extract from serialized information for third-party integrations + if serialized: + if "kwargs" in serialized: + ser_kwargs = serialized["kwargs"] + for model_tag in ("model", "model_id", "model_name"): + if (model := ser_kwargs.get(model_tag)) is not None: + return model + + for model_tag in ("model", "model_id", "model_name"): + if (model := serialized.get(model_tag)) is not None: + return model + + if "id" in serialized and serialized["id"]: + class_name = serialized["id"][-1] if isinstance(serialized["id"], list) else str(serialized["id"]) + return _infer_model_from_class_name(class_name, serialized) + + return _get_unified_unknown_model() + + +def _infer_model_from_class_name(class_name: str, serialized: dict) -> str: + """Infer model name from LangChain model class name for known third-party integrations.""" + + # For ChatDeepSeek, try to extract actual model from serialized kwargs + if "ChatDeepSeek" in class_name: + # Check if serialized contains the actual model name + if "kwargs" in serialized: + ser_kwargs = serialized["kwargs"] + # ChatDeepSeek might store model in different fields + for model_field in ("model", "_model", "model_name"): + if model_field in ser_kwargs and ser_kwargs[model_field]: + return ser_kwargs[model_field] + return _get_unified_unknown_model(class_name) + + if any(model_class in class_name for model_class in ["ChatOpenAI", "ChatAnthropic", "ChatCohere", "ChatOllama"]): + return _get_unified_unknown_model(class_name) + + return _get_unified_unknown_model() + + +def set_request_params( + span, kwargs, span_holder: SpanHolder, serialized: Optional[dict] = None, metadata: Optional[dict] = None +): + if not span.is_recording(): + return + + model = _extract_model_name_from_request(kwargs, span_holder, serialized, metadata) + span_holder.request_model = model _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, model) # response is not available for LLM requests (as opposed to chat) @@ -118,7 +231,7 @@ def set_llm_request( kwargs: Any, span_holder: SpanHolder, ) -> None: - set_request_params(span, kwargs, span_holder) + set_request_params(span, kwargs, span_holder, serialized) if should_send_prompts(): for i, msg in enumerate(prompts): @@ -141,7 +254,8 @@ def set_chat_request( kwargs: Any, span_holder: SpanHolder, ) -> None: - set_request_params(span, serialized.get("kwargs", {}), span_holder) + metadata = kwargs.get("metadata") + set_request_params(span, kwargs, span_holder, serialized, metadata) if should_send_prompts(): for i, function in enumerate( @@ -359,21 +473,44 @@ def set_chat_response_usage( ) -def extract_model_name_from_response_metadata(response: LLMResult) -> str: +def extract_model_name_from_response_metadata(response: LLMResult) -> Optional[str]: + """Enhanced model name extraction from response metadata with third-party support.""" + + # Standard extraction from response metadata for generations in response.generations: for generation in generations: if ( getattr(generation, "message", None) and getattr(generation.message, "response_metadata", None) - and (model_name := generation.message.response_metadata.get("model_name")) ): + metadata = generation.message.response_metadata + # Try multiple possible model name fields + for model_field in ("model_name", "model", "model_id"): + if (model_name := metadata.get(model_field)): + return model_name + + # Enhanced extraction for third-party models + # Check if llm_output contains model information + if response.llm_output: + for model_field in ("model", "model_name", "model_id"): + if (model_name := response.llm_output.get(model_field)): return model_name + # Check generation_info for model information + for generations in response.generations: + for generation in generations: + if hasattr(generation, "generation_info") and generation.generation_info: + for model_field in ("model", "model_name", "model_id"): + if (model_name := generation.generation_info.get(model_field)): + return model_name + + return None -def _extract_model_name_from_association_metadata(metadata: Optional[dict[str, Any]] = None) -> str: + +def _extract_model_name_from_association_metadata(metadata: Optional[dict[str, Any]] = None) -> Optional[str]: if metadata: - return metadata.get("ls_model_name") or "unknown" - return "unknown" + return metadata.get("ls_model_name") + return None def _set_chat_tool_calls( diff --git a/packages/opentelemetry-instrumentation-langchain/poetry.lock b/packages/opentelemetry-instrumentation-langchain/poetry.lock index 22f468d079..3ea99903a3 100644 --- a/packages/opentelemetry-instrumentation-langchain/poetry.lock +++ b/packages/opentelemetry-instrumentation-langchain/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -207,7 +207,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" groups = ["test"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, @@ -285,8 +285,8 @@ files = [ jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = [ - {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, + {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, ] [package.extras] @@ -304,6 +304,104 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["test"] +markers = "platform_python_implementation == \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "charset-normalizer" version = "3.4.0" @@ -494,7 +592,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev", "test"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -733,7 +831,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" groups = ["test"] -markers = "python_version <= \"3.12\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +markers = "(platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and python_version < \"3.13\"" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -1254,25 +1352,41 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10" [[package]] name = "langchain-core" -version = "0.3.63" +version = "0.3.76" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.9" groups = ["test"] files = [ - {file = "langchain_core-0.3.63-py3-none-any.whl", hash = "sha256:f91db8221b1bc6808f70b2e72fded1a94d50ee3f1dff1636fb5a5a514c64b7f5"}, - {file = "langchain_core-0.3.63.tar.gz", hash = "sha256:e2e30cfbb7684a5a0319f6cbf065fc3c438bfd1060302f085a122527890fb01e"}, + {file = "langchain_core-0.3.76-py3-none-any.whl", hash = "sha256:46e0eb48c7ac532432d51f8ca1ece1804c82afe9ae3dcf027b867edadf82b3ec"}, + {file = "langchain_core-0.3.76.tar.gz", hash = "sha256:71136a122dd1abae2c289c5809d035cf12b5f2bb682d8a4c1078cd94feae7419"}, ] [package.dependencies] jsonpatch = ">=1.33,<2.0" -langsmith = ">=0.1.126,<0.4" -packaging = ">=23.2,<25" +langsmith = ">=0.3.45" +packaging = ">=23.2" pydantic = ">=2.7.4" PyYAML = ">=5.3" tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0" typing-extensions = ">=4.7" +[[package]] +name = "langchain-deepseek" +version = "0.1.4" +description = "An integration package connecting DeepSeek and LangChain" +optional = false +python-versions = ">=3.9" +groups = ["test"] +files = [ + {file = "langchain_deepseek-0.1.4-py3-none-any.whl", hash = "sha256:9ce3dbfc7a40f221657ffe31e8623ea6e397f6c90de2a58d38204ac63e8f41ff"}, + {file = "langchain_deepseek-0.1.4.tar.gz", hash = "sha256:dc105138aee4fce03badd0521e69d5508b37f5c087d92b3e8481ffb8f9563d33"}, +] + +[package.dependencies] +langchain-core = ">=0.3.70,<1.0.0" +langchain-openai = ">=0.3.28,<1.0.0" + [[package]] name = "langchain-experimental" version = "0.3.2" @@ -1310,19 +1424,19 @@ transformers = ">=4.39.0" [[package]] name = "langchain-openai" -version = "0.3.14" +version = "0.3.33" description = "An integration package connecting OpenAI and LangChain" optional = false -python-versions = "<4.0,>=3.9" +python-versions = ">=3.9" groups = ["test"] files = [ - {file = "langchain_openai-0.3.14-py3-none-any.whl", hash = "sha256:b8e648d2d7678a5540818199d141ff727c6f1514294b3e1e999a95357c9d66a0"}, - {file = "langchain_openai-0.3.14.tar.gz", hash = "sha256:0662db78620c2e5c3ccfc1c36dc959c0ddc80e6bdf7ef81632cbf4b2cc9b9461"}, + {file = "langchain_openai-0.3.33-py3-none-any.whl", hash = "sha256:2d52aab6d2af61da9bb9470616ce782128f4be59df965caee3dece30ae6b2bc4"}, + {file = "langchain_openai-0.3.33.tar.gz", hash = "sha256:2dec058332ea9e8977cd91df6515b95952e187ac7484f349c3fe91d936a92375"}, ] [package.dependencies] -langchain-core = ">=0.3.53,<1.0.0" -openai = ">=1.68.2,<2.0.0" +langchain-core = ">=0.3.76,<1.0.0" +openai = ">=1.104.2,<2.0.0" tiktoken = ">=0.7,<1" [[package]] @@ -1427,25 +1541,33 @@ orjson = ">=3.10.1" [[package]] name = "langsmith" -version = "0.1.137" +version = "0.3.45" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false -python-versions = "<4.0,>=3.8.1" +python-versions = ">=3.9" groups = ["test"] files = [ - {file = "langsmith-0.1.137-py3-none-any.whl", hash = "sha256:4256d5c61133749890f7b5c88321dbb133ce0f440c621ea28e76513285859b81"}, - {file = "langsmith-0.1.137.tar.gz", hash = "sha256:56cdfcc6c74cb20a3f437d5bd144feb5bf93f54c5a2918d1e568cbd084a372d4"}, + {file = "langsmith-0.3.45-py3-none-any.whl", hash = "sha256:5b55f0518601fa65f3bb6b1a3100379a96aa7b3ed5e9380581615ba9c65ed8ed"}, + {file = "langsmith-0.3.45.tar.gz", hash = "sha256:1df3c6820c73ed210b2c7bc5cdb7bfa19ddc9126cd03fdf0da54e2e171e6094d"}, ] [package.dependencies] httpx = ">=0.23.0,<1" -orjson = ">=3.9.14,<4.0.0" +orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""} +packaging = ">=23.2" pydantic = [ {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, ] requests = ">=2,<3" requests-toolbelt = ">=1.0.0,<2.0.0" +zstandard = ">=0.23.0,<0.24.0" + +[package.extras] +langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"] +openai-agents = ["openai-agents (>=0.0.3,<0.1)"] +otel = ["opentelemetry-api (>=1.30.0,<2.0.0)", "opentelemetry-exporter-otlp-proto-http (>=1.30.0,<2.0.0)", "opentelemetry-sdk (>=1.30.0,<2.0.0)"] +pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"] [[package]] name = "markupsafe" @@ -1824,7 +1946,7 @@ description = "CUBLAS native runtime libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb"}, {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:235f728d6e2a409eddf1df58d5b0921cf80cfa9e72b9f2775ccb7b4a87984668"}, @@ -1838,7 +1960,7 @@ description = "CUDA profiling tools runtime libs." optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:166ee35a3ff1587f2490364f90eeeb8da06cd867bd5b701bf7f9a02b78bc63fc"}, {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.whl", hash = "sha256:358b4a1d35370353d52e12f0a7d1769fc01ff74a191689d3870b2123156184c4"}, @@ -1854,7 +1976,7 @@ description = "NVRTC native runtime libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5847f1d6e5b757f1d2b3991a01082a44aad6f10ab3c5c0213fa3e25bddc25a13"}, {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53"}, @@ -1868,7 +1990,7 @@ description = "CUDA Runtime native Libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6116fad3e049e04791c0256a9778c16237837c08b27ed8c8401e2e45de8d60cd"}, {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d461264ecb429c84c8879a7153499ddc7b19b5f8d84c204307491989a365588e"}, @@ -1884,7 +2006,7 @@ description = "cuDNN runtime libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9fd4584468533c61873e5fda8ca41bac3a38bcb2d12350830c69b0a96a7e4def"}, {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2"}, @@ -1901,7 +2023,7 @@ description = "CUFFT native runtime libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6"}, {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8510990de9f96c803a051822618d42bf6cb8f069ff3f48d93a8486efdacb48fb"}, @@ -1920,7 +2042,7 @@ description = "cuFile GPUDirect libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159"}, {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:8f57a0051dcf2543f6dc2b98a98cb2719c37d3cee1baba8965d57f3bbc90d4db"}, @@ -1933,7 +2055,7 @@ description = "CURAND native runtime libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6e82df077060ea28e37f48a3ec442a8f47690c7499bff392a5938614b56c98d8"}, {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf"}, @@ -1949,7 +2071,7 @@ description = "CUDA solver native runtime libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0"}, {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c"}, @@ -1970,7 +2092,7 @@ description = "CUSPARSE native runtime libraries" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887"}, {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7aa32fa5470cf754f72d1116c7cbc300b4e638d3ae5304cfa4a638a5b87161b1"}, @@ -1989,7 +2111,7 @@ description = "NVIDIA cuSPARSELt" optional = false python-versions = "*" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8371549623ba601a06322af2133c4a44350575f5a3108fb75f3ef20b822ad5f1"}, {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46"}, @@ -2003,7 +2125,7 @@ description = "NVIDIA Collective Communication Library (NCCL) Runtime" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c196e95e832ad30fbbb50381eb3cbd1fadd5675e587a548563993609af19522"}, {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6"}, @@ -2016,7 +2138,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a"}, {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41"}, @@ -2030,7 +2152,7 @@ description = "NVIDIA Tools Extension" optional = false python-versions = ">=3" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f44f8d86bb7d5629988d61c8d3ae61dddb2015dee142740536bc7481b022fe4b"}, {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:adcaabb9d436c9761fca2b13959a2d237c5f9fd406c8e4b723c695409ff88059"}, @@ -2041,14 +2163,14 @@ files = [ [[package]] name = "openai" -version = "1.78.1" +version = "1.108.1" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "openai-1.78.1-py3-none-any.whl", hash = "sha256:7368bf147ca499804cc408fe68cdb6866a060f38dec961bbc97b04f9d917907e"}, - {file = "openai-1.78.1.tar.gz", hash = "sha256:8b26b364531b100df1b961d03560042e5f5be11301d7d49a6cd1a2b9af824dca"}, + {file = "openai-1.108.1-py3-none-any.whl", hash = "sha256:952fc027e300b2ac23be92b064eac136a2bc58274cec16f5d2906c361340d59b"}, + {file = "openai-1.108.1.tar.gz", hash = "sha256:6648468c1aec4eacfa554001e933a9fa075f57bacfc27588c2e34456cee9fef9"}, ] [package.dependencies] @@ -2062,6 +2184,7 @@ tqdm = ">4" typing-extensions = ">=4.11,<5" [package.extras] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] @@ -2102,7 +2225,7 @@ wrapt = ">=1.0.0,<2.0.0" [[package]] name = "opentelemetry-instrumentation-bedrock" -version = "0.45.6" +version = "0.47.2" description = "OpenTelemetry Bedrock instrumentation" optional = false python-versions = ">=3.9,<4" @@ -2124,7 +2247,7 @@ url = "../opentelemetry-instrumentation-bedrock" [[package]] name = "opentelemetry-instrumentation-openai" -version = "0.45.6" +version = "0.47.2" description = "OpenTelemetry OpenAI instrumentation" optional = false python-versions = ">=3.9,<4" @@ -2648,6 +2771,19 @@ files = [ {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["test"] +markers = "implementation_name != \"PyPy\" and platform_python_implementation == \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + [[package]] name = "pydantic" version = "2.10.5" @@ -3648,7 +3784,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "test"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, @@ -3817,7 +3953,7 @@ description = "A language and compiler for custom Deep Learning operations" optional = false python-versions = "*" groups = ["test"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +markers = "platform_machine == \"x86_64\" and platform_system == \"Linux\"" files = [ {file = "triton-3.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fad99beafc860501d7fcc1fb7045d9496cbe2c882b1674640304949165a916e7"}, {file = "triton-3.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3161a2bf073d6b22c4e2f33f951f3e5e3001462b2570e6df9cd57565bdec2984"}, @@ -3842,7 +3978,7 @@ description = "Typing stubs for requests" optional = false python-versions = ">=3.7" groups = ["test"] -markers = "platform_python_implementation == \"PyPy\" or python_version < \"3.10\"" +markers = "platform_python_implementation == \"PyPy\" or python_version == \"3.9\"" files = [ {file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"}, {file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"}, @@ -3858,7 +3994,7 @@ description = "Typing stubs for requests" optional = false python-versions = ">=3.8" groups = ["test"] -markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.10\"" +markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\"" files = [ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, @@ -3874,7 +4010,7 @@ description = "Typing stubs for urllib3" optional = false python-versions = "*" groups = ["test"] -markers = "platform_python_implementation == \"PyPy\" or python_version < \"3.10\"" +markers = "platform_python_implementation == \"PyPy\" or python_version == \"3.9\"" files = [ {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, @@ -3927,7 +4063,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" groups = ["test"] -markers = "platform_python_implementation == \"PyPy\" or python_version < \"3.10\"" +markers = "platform_python_implementation == \"PyPy\" or python_version == \"3.9\"" files = [ {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, @@ -3945,7 +4081,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.8" groups = ["test"] -markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.10\"" +markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\"" files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, @@ -3972,8 +4108,8 @@ files = [ [package.dependencies] PyYAML = "*" urllib3 = [ - {version = "*", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.10\""}, {version = "<2", markers = "platform_python_implementation == \"PyPy\" or python_version < \"3.10\""}, + {version = "*", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.10\""}, ] wrapt = "*" yarl = "*" @@ -4311,10 +4447,123 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] +[[package]] +name = "zstandard" +version = "0.23.0" +description = "Zstandard bindings for Python" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, + {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c"}, + {file = "zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813"}, + {file = "zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473"}, + {file = "zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160"}, + {file = "zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35"}, + {file = "zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d"}, + {file = "zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33"}, + {file = "zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd"}, + {file = "zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e"}, + {file = "zstandard-0.23.0-cp38-cp38-win32.whl", hash = "sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9"}, + {file = "zstandard-0.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5"}, + {file = "zstandard-0.23.0-cp39-cp39-win32.whl", hash = "sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274"}, + {file = "zstandard-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58"}, + {file = "zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + [extras] instruments = [] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4" -content-hash = "e80cc2e8dce5dabf8955c32872f48ce503e27dfb1dedc36801bd607f68ba0cd1" +content-hash = "adfa324f3f8ae7d1437a646f9d392f64244defe52cc8aa822bfb88d09d69c88d" diff --git a/packages/opentelemetry-instrumentation-langchain/pyproject.toml b/packages/opentelemetry-instrumentation-langchain/pyproject.toml index 56d0c963ef..a893e9bd01 100644 --- a/packages/opentelemetry-instrumentation-langchain/pyproject.toml +++ b/packages/opentelemetry-instrumentation-langchain/pyproject.toml @@ -54,6 +54,7 @@ boto3 = "^1.35.49" langchain-anthropic = "^0.3.13" langchain-aws = "^0.2.11" langchain-openai = "^0.3.1" +langchain-deepseek = "^0.1.4" langchain-cohere = "0.3.1" langchain-huggingface = "^0.1.2" pydantic = "^2.10.5" diff --git a/packages/opentelemetry-instrumentation-langchain/tests/conftest.py b/packages/opentelemetry-instrumentation-langchain/tests/conftest.py index 01930dca3d..fa01420d66 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/conftest.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/conftest.py @@ -26,7 +26,7 @@ from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -pytest_plugins = [] +pytest_plugins = ["pytest_recording"] @pytest.fixture(scope="session", name="span_exporter") diff --git a/packages/opentelemetry-instrumentation-langchain/tests/metrics/cassettes/test_langchain_metrics/test_streaming_with_ttft_and_generation_time_metrics.yaml b/packages/opentelemetry-instrumentation-langchain/tests/metrics/cassettes/test_langchain_metrics/test_streaming_with_ttft_and_generation_time_metrics.yaml new file mode 100644 index 0000000000..607cf66b2c --- /dev/null +++ b/packages/opentelemetry-instrumentation-langchain/tests/metrics/cassettes/test_langchain_metrics/test_streaming_with_ttft_and_generation_time_metrics.yaml @@ -0,0 +1,219 @@ +interactions: +- request: + body: '{"messages": [{"content": "Tell me about machine learning in one sentence", + "role": "user"}], "model": "deepseek-chat", "stream": true, "temperature": 0.7}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '155' + content-type: + - application/json + host: + - api.deepseek.com + user-agent: + - OpenAI/Python 1.107.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.107.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.4 + method: POST + uri: https://api.deepseek.com/beta/chat/completions + response: + body: + string: 'data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":"Machine"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + learning"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + is"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + the"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + science"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + of"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + getting"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + computers"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + to"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + learn"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + and"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + act"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + like"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + humans"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + do"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + by"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + feeding"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + them"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + data"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + so"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + they"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + can"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + improve"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + their"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + skills"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + over"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + time"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + without"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + being"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + explicitly"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + programmed"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + for"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + every"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":" + task"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"307419a8-2192-4989-84e7-65af318648c6","object":"chat.completion.chunk","created":1758412658,"model":"deepseek-chat","system_fingerprint":"fp_08f168e49b_prod0820_fp8_kvcache","choices":[{"index":0,"delta":{"content":""},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":35,"total_tokens":47,"prompt_tokens_details":{"cached_tokens":0},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":12}} + + + data: [DONE] + + + ' + headers: + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sat, 20 Sep 2025 23:57:38 GMT + Server: + - CW + Set-Cookie: + - HWWAFSESTIME=1758412658203; path=/ + - HWWAFSESID=c458276dfb3ffd1fc4; path=/ + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-credentials: + - 'true' + cache-control: + - no-cache + vary: + - origin, access-control-request-method, access-control-request-headers + x-ds-trace-id: + - 3e038ffe3306315e3455c2328e839ce7 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py b/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py index 22963ce9a3..655fb2acc0 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py @@ -5,6 +5,8 @@ from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI from opentelemetry.semconv_ai import Meters, SpanAttributes +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv._incubating.metrics import gen_ai_metrics as GenAIMetrics from langgraph.graph import StateGraph from openai import OpenAI @@ -121,6 +123,42 @@ def test_llm_chain_streaming_metrics(instrument_legacy, reader, llm): == "openai" ) + if metric.name == Meters.LLM_GENERATION_CHOICES: + assert any( + data_point.value >= 1 for data_point in metric.data.data_points + ) + for data_point in metric.data.data_points: + assert ( + data_point.attributes[SpanAttributes.LLM_SYSTEM] + == "openai" + ) + + if metric.name == GenAIMetrics.GEN_AI_SERVER_TIME_TO_FIRST_TOKEN: + assert any( + data_point.count > 0 for data_point in metric.data.data_points + ) + assert any( + data_point.sum > 0 for data_point in metric.data.data_points + ) + for data_point in metric.data.data_points: + assert ( + data_point.attributes[SpanAttributes.LLM_SYSTEM] + == "openai" + ) + + if metric.name == Meters.LLM_STREAMING_TIME_TO_GENERATE: + assert any( + data_point.count > 0 for data_point in metric.data.data_points + ) + assert any( + data_point.sum > 0 for data_point in metric.data.data_points + ) + for data_point in metric.data.data_points: + assert ( + data_point.attributes[SpanAttributes.LLM_SYSTEM] + == "openai" + ) + assert found_token_metric is True assert found_duration_metric is True @@ -265,3 +303,148 @@ def calculate(state: State): == "openai" ) assert data_point.value > 0 + + +@pytest.mark.vcr +def test_streaming_with_ttft_and_generation_time_metrics(instrument_legacy, reader): + """Test streaming metrics with ChatDeepSeek to validate our third-party model fixes.""" + from langchain_core.prompts import ChatPromptTemplate + from langchain_deepseek import ChatDeepSeek + + llm = ChatDeepSeek( + api_key="", + api_base="https://api.deepseek.com/beta", + model="deepseek-chat", + temperature=0.7, + streaming=True + ) + + prompt = ChatPromptTemplate.from_template("Tell me about {topic} in one sentence") + chain = prompt | llm + + # Stream the response to trigger on_llm_new_token calls + response_chunks = [] + for chunk in chain.stream({"topic": "machine learning"}): + response_chunks.append(chunk) + + # Verify we got streaming chunks + assert len(response_chunks) > 1, "Should have multiple chunks for streaming" + + metrics_data = reader.get_metrics_data() + resource_metrics = metrics_data.resource_metrics + assert len(resource_metrics) > 0 + + found_token_metric = False + found_duration_metric = False + found_choices_metric = False + found_ttft_metric = False + found_streaming_time_metric = False + + for rm in resource_metrics: + for sm in rm.scope_metrics: + for metric in sm.metrics: + if metric.name == Meters.LLM_TOKEN_USAGE: + found_token_metric = True + # Verify ChatDeepSeek model name is correctly extracted + for data_point in metric.data.data_points: + assert data_point.attributes[SpanAttributes.LLM_SYSTEM] == "Langchain" + assert data_point.attributes[SpanAttributes.LLM_RESPONSE_MODEL] == "deepseek-chat" + assert data_point.attributes[SpanAttributes.LLM_TOKEN_TYPE] in ["input", "output"] + + elif metric.name == Meters.LLM_OPERATION_DURATION: + found_duration_metric = True + assert any( + data_point.sum > 0 for data_point in metric.data.data_points + ) + # Verify ChatDeepSeek model name + for data_point in metric.data.data_points: + assert data_point.attributes[SpanAttributes.LLM_SYSTEM] == "Langchain" + assert data_point.attributes[SpanAttributes.LLM_RESPONSE_MODEL] == "deepseek-chat" + + elif metric.name == Meters.LLM_GENERATION_CHOICES: + found_choices_metric = True + assert any( + data_point.value >= 1 for data_point in metric.data.data_points + ) + # Verify ChatDeepSeek model name + for data_point in metric.data.data_points: + assert data_point.attributes[SpanAttributes.LLM_SYSTEM] == "Langchain" + assert data_point.attributes[SpanAttributes.LLM_RESPONSE_MODEL] == "deepseek-chat" + + elif metric.name == GenAIMetrics.GEN_AI_SERVER_TIME_TO_FIRST_TOKEN: + found_ttft_metric = True + assert any( + data_point.count > 0 for data_point in metric.data.data_points + ) + assert any( + data_point.sum > 0 for data_point in metric.data.data_points + ) + # Verify our ChatDeepSeek fixes work + for data_point in metric.data.data_points: + assert data_point.attributes[SpanAttributes.LLM_SYSTEM] == "Langchain" + assert data_point.attributes[SpanAttributes.LLM_RESPONSE_MODEL] == "deepseek-chat" + + elif metric.name == Meters.LLM_STREAMING_TIME_TO_GENERATE: + found_streaming_time_metric = True + assert any( + data_point.count > 0 for data_point in metric.data.data_points + ) + # Verify our ChatDeepSeek fixes work + for data_point in metric.data.data_points: + assert data_point.attributes[SpanAttributes.LLM_SYSTEM] == "Langchain" + assert data_point.attributes[SpanAttributes.LLM_RESPONSE_MODEL] == "deepseek-chat" + + elif metric.name == "llm.langchain.completions.exceptions": + pass + + assert found_token_metric is True + assert found_duration_metric is True + assert found_choices_metric is True + + # Since this test specifically uses ChatDeepSeek with streaming=True to validate + # our TTFT implementation, these metrics MUST be present + assert found_ttft_metric is True, "TTFT metric should be present with ChatDeepSeek streaming" + assert found_streaming_time_metric is True, "Streaming time metric should be present with ChatDeepSeek streaming" + + print("All ChatDeepSeek streaming metrics validated successfully") + + +def test_exception_metrics(instrument_legacy, reader): + """Test that exception metrics are recorded when LLM calls fail.""" + from unittest.mock import patch + + llm = ChatOpenAI(model="gpt-3.5-turbo") + chain = LLMChain( + llm=llm, + prompt=PromptTemplate( + input_variables=["product"], + template="What is a good name for a company that makes {product}?", + ) + ) + + # Mock the LLM to raise an exception + with patch.object(llm, '_generate', side_effect=Exception("API Error")): + try: + chain.run(product="test") + except Exception: + pass # Expected to fail + + metrics_data = reader.get_metrics_data() + resource_metrics = metrics_data.resource_metrics + assert len(resource_metrics) > 0 + + found_exception_metric = False + + for rm in resource_metrics: + for sm in rm.scope_metrics: + for metric in sm.metrics: + if metric.name == "llm.langchain.completions.exceptions": + found_exception_metric = True + assert any( + data_point.value >= 1 for data_point in metric.data.data_points + ) + # Check that error attributes are set + for data_point in metric.data.data_points: + assert "error.type" in data_point.attributes or ERROR_TYPE in data_point.attributes + + assert found_exception_metric is True diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_model_extraction.py b/packages/opentelemetry-instrumentation-langchain/tests/test_model_extraction.py new file mode 100644 index 0000000000..d9b3120640 --- /dev/null +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_model_extraction.py @@ -0,0 +1,203 @@ +from unittest.mock import Mock +from opentelemetry.instrumentation.langchain.span_utils import ( + _extract_model_name_from_request, + _infer_model_from_class_name, + extract_model_name_from_response_metadata, + SpanHolder, +) +from langchain_core.outputs import LLMResult, Generation, ChatGeneration +from langchain_core.messages import AIMessage + + +class TestModelExtraction: + """Test enhanced model name extraction for third-party integrations.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_span = Mock() + self.span_holder = SpanHolder( + span=self.mock_span, + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test" + ) + + def test_standard_model_extraction_from_kwargs(self): + """Test standard model extraction from kwargs.""" + kwargs = {"model": "gpt-4"} + + result = _extract_model_name_from_request(kwargs, self.span_holder) + assert result == "gpt-4" + + def test_model_extraction_from_invocation_params(self): + """Test model extraction from invocation_params.""" + kwargs = { + "invocation_params": { + "model": "claude-3-sonnet" + } + } + + result = _extract_model_name_from_request(kwargs, self.span_holder) + assert result == "claude-3-sonnet" + + def test_deepseek_model_extraction_from_serialized(self): + """Test ChatDeepSeek model extraction from serialized data.""" + kwargs = {} + serialized = { + "id": ["langchain", "chat_models", "ChatDeepSeek"], + "kwargs": { + "model": "deepseek-coder", + "api_base": "https://api.deepseek.com/beta" + } + } + + result = _extract_model_name_from_request(kwargs, self.span_holder, serialized) + assert result == "deepseek-coder" + + def test_deepseek_fallback_when_no_model_in_serialized(self): + """Test ChatDeepSeek fallback to default model.""" + kwargs = {} + serialized = { + "id": ["langchain", "chat_models", "ChatDeepSeek"], + "kwargs": { + "api_base": "https://api.deepseek.com/beta" + # No model field + } + } + + result = _extract_model_name_from_request(kwargs, self.span_holder, serialized) + assert result == "deepseek-unknown" + + def test_class_name_inference_for_known_models(self): + """Test model inference from class names for known integrations.""" + test_cases = [ + ("ChatOpenAI", {}, "gpt-unknown"), + ("ChatAnthropic", {}, "claude-unknown"), + ("ChatCohere", {}, "command-unknown"), + ("ChatOllama", {}, "ollama-unknown"), + ] + + for class_name, serialized, expected in test_cases: + result = _infer_model_from_class_name(class_name, serialized) + assert result == expected, f"Failed for {class_name}" + + def test_unknown_class_returns_unknown(self): + """Test that unknown class names return unknown.""" + result = _infer_model_from_class_name("SomeUnknownModel", {}) + assert result == "unknown" + + def test_enhanced_response_metadata_extraction(self): + """Test enhanced response metadata extraction.""" + + # Test with response_metadata + message = AIMessage( + content="Test response", + response_metadata={"model": "deepseek-v2"} + ) + generation = ChatGeneration(message=message) + response = LLMResult(generations=[[generation]]) + + result = extract_model_name_from_response_metadata(response) + assert result == "deepseek-v2" + + def test_response_extraction_from_llm_output(self): + """Test model extraction from llm_output.""" + response = LLMResult( + generations=[[Generation(text="Test")]], + llm_output={"model": "deepseek-coder-v2"} + ) + + result = extract_model_name_from_response_metadata(response) + assert result == "deepseek-coder-v2" + + def test_response_extraction_from_generation_info(self): + """Test model extraction from generation_info.""" + generation = Generation( + text="Test response", + generation_info={"model_name": "deepseek-reasoner"} + ) + response = LLMResult(generations=[[generation]]) + + result = extract_model_name_from_response_metadata(response) + assert result == "deepseek-reasoner" + + def test_model_extraction_priority_order(self): + """Test that model extraction follows correct priority order.""" + # kwargs should have highest priority + kwargs = {"model": "from-kwargs"} + serialized = { + "id": ["ChatDeepSeek"], + "kwargs": {"model": "from-serialized"} + } + + result = _extract_model_name_from_request(kwargs, self.span_holder, serialized) + assert result == "from-kwargs" + + # invocation_params should be second priority + kwargs = { + "invocation_params": {"model": "from-invocation-params"}, + "kwargs": {"model": "from-nested-kwargs"} + } + + result = _extract_model_name_from_request(kwargs, self.span_holder, serialized) + assert result == "from-invocation-params" + + def test_nested_kwargs_extraction(self): + """Test extraction from nested kwargs structures.""" + kwargs = { + "kwargs": {"model": "nested-model"} + } + + result = _extract_model_name_from_request(kwargs, self.span_holder) + assert result == "nested-model" + + def test_model_kwargs_extraction(self): + """Test extraction from model_kwargs.""" + kwargs = { + "model_kwargs": {"model_name": "model-from-kwargs"} + } + + result = _extract_model_name_from_request(kwargs, self.span_holder) + assert result == "model-from-kwargs" + + def test_no_model_info_returns_unknown(self): + """Test that missing model info returns unknown.""" + kwargs = {} + serialized = {"id": ["UnknownModel"]} + + result = _extract_model_name_from_request(kwargs, self.span_holder, serialized) + assert result == "unknown" + + def test_association_metadata_extraction(self): + """Test model extraction from association metadata (ChatDeepSeek pattern).""" + kwargs = {} + metadata = {"ls_model_name": "deepseek-reasoner"} + + result = _extract_model_name_from_request(kwargs, self.span_holder, None, metadata) + assert result == "deepseek-reasoner" + + def test_metadata_has_priority_over_class_inference(self): + """Test that association metadata has higher priority than class inference.""" + kwargs = {} + serialized = { + "id": ["ChatDeepSeek"], + "kwargs": {} # No model in serialized kwargs + } + metadata = {"ls_model_name": "deepseek-v3"} + + result = _extract_model_name_from_request(kwargs, self.span_holder, serialized, metadata) + assert result == "deepseek-v3" # Should use metadata, not fallback to "deepseek-chat" + + def test_response_metadata_extraction_returns_none_when_no_model_found(self): + """Test that extract_model_name_from_response_metadata returns None when no model info is found.""" + # Response with no model information anywhere + response = LLMResult( + generations=[[Generation(text="Response without model info")]], + llm_output={"other_field": "value"} # No model-related fields + ) + + result = extract_model_name_from_response_metadata(response) + assert result is None, "Should return None when no model information is found" diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_streaming_metrics.py b/packages/opentelemetry-instrumentation-langchain/tests/test_streaming_metrics.py new file mode 100644 index 0000000000..10ccfa8c76 --- /dev/null +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_streaming_metrics.py @@ -0,0 +1,242 @@ +import time +from unittest.mock import Mock, patch +from uuid import uuid4 +from langchain_core.outputs import LLMResult, Generation +from opentelemetry.instrumentation.langchain.callback_handler import TraceloopCallbackHandler +from opentelemetry.instrumentation.langchain.span_utils import SpanHolder +from opentelemetry.semconv_ai import SpanAttributes +from opentelemetry.trace import Span + + +class TestStreamingMetrics: + """Test the new streaming metrics functionality.""" + + def setup_method(self): + """Set up test fixtures.""" + self.tracer = Mock() + self.duration_histogram = Mock() + self.token_histogram = Mock() + self.ttft_histogram = Mock() + self.streaming_time_histogram = Mock() + self.choices_counter = Mock() + self.exception_counter = Mock() + + self.handler = TraceloopCallbackHandler( + self.tracer, + self.duration_histogram, + self.token_histogram, + self.ttft_histogram, + self.streaming_time_histogram, + self.choices_counter, + self.exception_counter, + ) + + def test_ttft_metric_recorded_on_first_token(self): + """Test that TTFT metric is recorded when first token arrives.""" + run_id = uuid4() + mock_span = Mock(spec=Span) + mock_span.attributes = {SpanAttributes.LLM_SYSTEM: "Langchain"} + + # Use mock time for stable testing + with patch('opentelemetry.instrumentation.langchain.callback_handler.time.time') as mock_time, \ + patch('opentelemetry.instrumentation.langchain.span_utils.time.time') as mock_span_time: + + start_time = 1000.0 + mock_time.return_value = start_time + mock_span_time.return_value = start_time + + span_holder = SpanHolder( + span=mock_span, + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test", + start_time=start_time + ) + self.handler.spans[run_id] = span_holder + + mock_time.return_value = start_time + 0.1 + mock_span_time.return_value = start_time + 0.1 + self.handler.on_llm_new_token("Hello", run_id=run_id) + + self.ttft_histogram.record.assert_called_once() + args = self.ttft_histogram.record.call_args + ttft_value = args[0][0] + assert abs(ttft_value - 0.1) < 0.001, f"TTFT should be approximately 0.1 seconds, got {ttft_value}" + + attributes = args[1]["attributes"] + assert attributes[SpanAttributes.LLM_SYSTEM] == "Langchain" + + def test_ttft_metric_not_recorded_on_subsequent_tokens(self): + """Test that TTFT metric is only recorded once.""" + run_id = uuid4() + mock_span = Mock(spec=Span) + mock_span.attributes = {SpanAttributes.LLM_SYSTEM: "Langchain"} + + span_holder = SpanHolder( + span=mock_span, + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test", + start_time=time.time() + ) + self.handler.spans[run_id] = span_holder + + # First token + self.handler.on_llm_new_token("Hello", run_id=run_id) + # Second token + self.handler.on_llm_new_token(" world", run_id=run_id) + + # TTFT should only be recorded once + assert self.ttft_histogram.record.call_count == 1 + + def test_generation_choices_metric_recorded(self): + """Test that generation choices metric is recorded.""" + run_id = uuid4() + mock_span = Mock(spec=Span) + mock_span.attributes = {SpanAttributes.LLM_SYSTEM: "Langchain"} + + span_holder = SpanHolder( + span=mock_span, + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test", + start_time=time.time() + ) + self.handler.spans[run_id] = span_holder + + # Mock LLMResult with multiple generations + generation1 = Generation(text="Response 1") + generation2 = Generation(text="Response 2") + llm_result = LLMResult( + generations=[[generation1, generation2]], + llm_output={"model_name": "test-model"} + ) + + self.handler.on_llm_end(llm_result, run_id=run_id) + + # Verify choices counter was called + self.choices_counter.add.assert_called_once_with( + 2, # Two choices + attributes={ + SpanAttributes.LLM_SYSTEM: "Langchain", + SpanAttributes.LLM_RESPONSE_MODEL: "test-model", + } + ) + + def test_streaming_time_to_generate_metric(self): + """Test that streaming time to generate metric is recorded.""" + run_id = uuid4() + mock_span = Mock(spec=Span) + mock_span.attributes = {SpanAttributes.LLM_SYSTEM: "Langchain"} + + with patch('opentelemetry.instrumentation.langchain.callback_handler.time.time') as mock_time, \ + patch('opentelemetry.instrumentation.langchain.span_utils.time.time') as mock_span_time: + + start_time = 1000.0 + mock_time.return_value = start_time + mock_span_time.return_value = start_time + + span_holder = SpanHolder( + span=mock_span, + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test", + start_time=start_time + ) + self.handler.spans[run_id] = span_holder + + first_token_time = start_time + 0.05 + mock_time.return_value = first_token_time + mock_span_time.return_value = first_token_time + self.handler.on_llm_new_token("Hello", run_id=run_id) + + completion_time = first_token_time + 0.05 + mock_time.return_value = completion_time + mock_span_time.return_value = completion_time + llm_result = LLMResult( + generations=[[Generation(text="Hello world")]], + llm_output={"model_name": "test-model"} + ) + + self.handler.on_llm_end(llm_result, run_id=run_id) + + self.streaming_time_histogram.record.assert_called_once() + args = self.streaming_time_histogram.record.call_args + streaming_time = args[0][0] + assert abs(streaming_time - 0.05) < 0.001, ( + f"Streaming time should be approximately 0.05 seconds, " + f"got {streaming_time}" + ) + + def test_exception_metric_recorded_on_error(self): + """Test that exception metric is recorded on LLM errors.""" + run_id = uuid4() + mock_span = Mock(spec=Span) + mock_span.attributes = {SpanAttributes.LLM_SYSTEM: "Langchain"} + + span_holder = SpanHolder( + span=mock_span, + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test", + start_time=time.time() + ) + self.handler.spans[run_id] = span_holder + + # Simulate error + error = ValueError("API Error") + self.handler.on_llm_error(error, run_id=run_id) + + # Verify exception counter was called + self.exception_counter.add.assert_called_once_with( + 1, + attributes={ + SpanAttributes.LLM_SYSTEM: "Langchain", + SpanAttributes.LLM_RESPONSE_MODEL: "unknown", + "error.type": "ValueError", + } + ) + + def test_no_ttft_when_no_first_token_time(self): + """Test streaming time metric is not recorded without first token.""" + run_id = uuid4() + mock_span = Mock(spec=Span) + mock_span.attributes = {SpanAttributes.LLM_SYSTEM: "Langchain"} + + span_holder = SpanHolder( + span=mock_span, + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test", + start_time=time.time() + ) + # No first_token_time set + self.handler.spans[run_id] = span_holder + + llm_result = LLMResult( + generations=[[Generation(text="Response")]], + llm_output={"model_name": "test-model"} + ) + + self.handler.on_llm_end(llm_result, run_id=run_id) + + # Streaming time metric should not be recorded + self.streaming_time_histogram.record.assert_not_called() diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_third_party_models.py b/packages/opentelemetry-instrumentation-langchain/tests/test_third_party_models.py new file mode 100644 index 0000000000..ed1fc0dbbc --- /dev/null +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_third_party_models.py @@ -0,0 +1,143 @@ +from unittest.mock import Mock +from uuid import uuid4 +from opentelemetry.instrumentation.langchain.callback_handler import TraceloopCallbackHandler +from opentelemetry.instrumentation.langchain.span_utils import SpanHolder +from opentelemetry.semconv_ai import SpanAttributes +from langchain_core.outputs import LLMResult, Generation + + +class TestThirdPartyModels: + """Test enhanced model extraction for third-party LangChain integrations like ChatDeepSeek.""" + + def setup_method(self): + self.tracer = Mock() + self.duration_histogram = Mock() + self.token_histogram = Mock() + self.ttft_histogram = Mock() + self.streaming_time_histogram = Mock() + self.choices_counter = Mock() + self.exception_counter = Mock() + + self.handler = TraceloopCallbackHandler( + self.tracer, + self.duration_histogram, + self.token_histogram, + self.ttft_histogram, + self.streaming_time_histogram, + self.choices_counter, + self.exception_counter, + ) + + def test_chatdeepseek_support(self): + """Test model extraction and streaming metrics for models that store info in serialized kwargs.""" + run_id = uuid4() + + # Test model extraction from serialized kwargs + serialized = { + "id": ["langchain_deepseek", "chat_models", "ChatDeepSeek"], + "kwargs": { + "model": "deepseek-reasoner", + "api_base": "https://api.deepseek.com/beta", + "temperature": 0.7 + } + } + + kwargs = { + "invocation_params": { + "temperature": 0.7 + } + } + + # Start chat model + self.handler.on_chat_model_start( + serialized=serialized, + messages=[], + run_id=run_id, + kwargs=kwargs + ) + + # Verify model extraction + assert run_id in self.handler.spans + span_holder = self.handler.spans[run_id] + assert span_holder.request_model == "deepseek-reasoner" + + span = span_holder.span + span.set_attribute.assert_any_call(SpanAttributes.LLM_REQUEST_MODEL, "deepseek-reasoner") + + span_attrs = { + SpanAttributes.LLM_SYSTEM: "Langchain", + SpanAttributes.LLM_REQUEST_TYPE: "chat" + } + span.attributes = Mock() + span.attributes.get = lambda key, default=None: span_attrs.get(key, default) + + # Test fallback behavior when no model in serialized kwargs + fallback_serialized = { + "id": ["langchain_deepseek", "chat_models", "ChatDeepSeek"], + "kwargs": { + "api_base": "https://api.deepseek.com/beta" + # No model field + } + } + + run_id_fallback = uuid4() + self.handler.on_chat_model_start( + serialized=fallback_serialized, + messages=[], + run_id=run_id_fallback, + kwargs={} + ) + + span_holder_fallback = self.handler.spans[run_id_fallback] + assert span_holder_fallback.request_model == "deepseek-unknown" + + # Test response processing without model info in llm_output + response = LLMResult( + generations=[[Generation(text="Response")]], + llm_output={ + "token_usage": { + "prompt_tokens": 10, + "completion_tokens": 15, + "total_tokens": 25 + } + } + ) + + self.handler.on_llm_end(response, run_id=run_id) + + # Verify that metrics use correct model name from request fallback + token_calls = self.token_histogram.record.call_args_list + + # Verify both calls use correct model name from request fallback + assert len(token_calls) == 2, f"Expected 2 token calls, got {len(token_calls)}" + + for call in token_calls: + attributes = call[1]["attributes"] + assert attributes[SpanAttributes.LLM_RESPONSE_MODEL] == "deepseek-reasoner" + + # Test model variant extraction + from opentelemetry.instrumentation.langchain.span_utils import _extract_model_name_from_request + + variant_serialized = { + "id": ["langchain_deepseek", "ChatDeepSeek"], + "kwargs": { + "model": "deepseek-coder-v2", + "api_base": "https://api.deepseek.com/beta" + } + } + + test_span_holder = SpanHolder( + span=Mock(), + token=None, + context=None, + children=[], + workflow_name="test", + entity_name="test", + entity_path="test" + ) + + test_models = ["deepseek-chat", "deepseek-coder", "deepseek-reasoner", "deepseek-coder-v2"] + for model in test_models: + variant_serialized["kwargs"]["model"] = model + result = _extract_model_name_from_request({}, test_span_holder, variant_serialized) + assert result == model, f"Failed to extract {model}"