Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/fortifyroot/_vendor/VENDOR_DEPENDENCIES.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
},
"test": {
"anthropic": ">=0.25.2,<0.83.0",
"boto3": ">=1.35.49,<2",
"boto3": ">=1.34.120,<2",
"chromadb": ">=0.5.23,<0.6.0",
"google-genai": ">=1.58.0,<1.59.0",
"langchain": ">=1.0.0,<2.0.0",
Expand Down Expand Up @@ -83,7 +83,7 @@
"pytest": ">=8.2.2,<9",
"pytest-asyncio": ">=0.23.7,<1.4.0",
"pytest-recording": ">=0.13.1,<0.14.0",
"pytest-sugar": "==1.1.1",
"pytest-sugar": "==1.0.0",
"requests": ">=2.31.0,<3",
"sqlalchemy": ">=2.0.31,<3",
"vcrpy": ">=8.0.0,<9"
Expand Down
6 changes: 3 additions & 3 deletions src/fortifyroot/_vendor/VENDOR_MANIFEST.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"vendored_at": "2026-06-19T15:31:31.520765",
"vendored_at": "2026-06-19T16:42:00.072255",
"openllmetry_version": "0.52.6",
"git_commit": "c011bcf0077e",
"git_commit": "c42896177f36",
"git_branch": "HEAD",
"git_tag": "fr-v0.52.6.26",
"git_tag": "fr-v0.52.6.27",
"instrumentation_package_policy": {
"opentelemetry-instrumentation-agno": false,
"opentelemetry-instrumentation-alephalpha": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE

FR_STREAMING_TIME_TO_FIRST_TOKEN_MS = (
"fortifyroot.llm.streaming.time_to_first_token_ms"
)
FR_STREAMING_TIME_TO_GENERATE_MS = "fortifyroot.llm.streaming.time_to_generate_ms"


def _extract_class_name_from_serialized(serialized: Optional[dict[str, Any]]) -> str:
"""
Expand Down Expand Up @@ -596,6 +601,38 @@ def on_llm_start(
else:
set_llm_request(span, serialized, prompts, kwargs, self.spans[run_id])

@dont_throw
def on_llm_new_token(
self,
token: str,
*,
run_id: UUID,
parent_run_id: Union[UUID, None] = None,
chunk: Union[GenerationChunk, ChatGenerationChunk, None] = None,
**kwargs: Any,
) -> Any:
"""Run when a streaming LLM emits a new token."""
if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
return
if not token and chunk is None:
return
if run_id not in self.spans:
return

span_holder = self.spans[run_id]
if span_holder.streaming_first_token_time is not None:
return

first_token_time = time.time()
span_holder.streaming_first_token_time = first_token_time
span = span_holder.span
if span.is_recording():
_set_span_attribute(
span,
FR_STREAMING_TIME_TO_FIRST_TOKEN_MS,
round((first_token_time - span_holder.start_time) * 1000),
)

@dont_throw
def on_llm_end(
self,
Expand Down Expand Up @@ -698,7 +735,15 @@ def on_llm_end(
set_chat_response(span, response)

# Record duration before ending span
duration = time.time() - self.spans[run_id].start_time
end_time = time.time()
streaming_first_token_time = self.spans[run_id].streaming_first_token_time
if streaming_first_token_time is not None and span.is_recording():
_set_span_attribute(
span,
FR_STREAMING_TIME_TO_GENERATE_MS,
round((end_time - streaming_first_token_time) * 1000),
)
duration = end_time - self.spans[run_id].start_time
vendor = span.attributes.get(GenAIAttributes.GEN_AI_SYSTEM, "Langchain")
self.duration_histogram.record(
duration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class SpanHolder:
entity_path: str
start_time: float = field(default_factory=time.time)
request_model: Optional[str] = None
streaming_first_token_time: Optional[float] = None
# ST-10 review-round-2 fix (2026-05-11): every context_api.attach()
# performed for this span — span-context, suppression, metadata
# association_properties — appended here in attach order. ``_end_span``
Expand Down
Loading