3333 ValidationResponse ,
3434)
3535from src .api .security import api_key_auth , audit_logger
36+ from src .telemetry import LocalFileStorage , TelemetryCollector , TelemetryEvent
3637from src .utils .openrouter_llm import create_openrouter_llm , get_model_name
3738from src .utils .schema_loader import HedSchemaLoader
3839from src .validation .hed_validator import HedPythonValidator
4546vision_agent : VisionAgent | None = None
4647schema_loader : HedSchemaLoader | None = None
4748
49+ # Telemetry collector (initialized in lifespan)
50+ telemetry_collector : TelemetryCollector | None = None
51+
4852# Cache for BYOK configuration
4953_byok_config : dict = {}
5054
@@ -389,6 +393,18 @@ def get_default_path(docker_path: str, local_path: str) -> str:
389393 else :
390394 print ("Vision model not available (only supported with OpenRouter)" )
391395
396+ # Initialize telemetry collector
397+ global telemetry_collector
398+ # Use /app/telemetry in Docker, otherwise use local .hedit/telemetry
399+ default_telemetry_dir = "/app/telemetry" if Path ("/app" ).exists () else ".hedit/telemetry"
400+ telemetry_dir = os .getenv ("TELEMETRY_DIR" , default_telemetry_dir )
401+ telemetry_storage = LocalFileStorage (storage_dir = telemetry_dir )
402+ telemetry_collector = TelemetryCollector (
403+ storage = telemetry_storage ,
404+ enabled = True , # Can be configured via env var if needed
405+ )
406+ print (f"Telemetry collector initialized (storage: { telemetry_dir } )" )
407+
392408 yield
393409
394410 # Shutdown
@@ -573,20 +589,50 @@ async def annotate(
573589 # LangGraph default is 25, increase to 100 for complex workflows
574590 config = {"recursion_limit" : 100 }
575591
592+ start_time = time .time ()
576593 final_state = await active_workflow .run (
577594 input_description = request .description ,
578595 schema_version = request .schema_version ,
579596 max_validation_attempts = request .max_validation_attempts ,
580597 run_assessment = request .run_assessment ,
581598 config = config ,
582599 )
600+ latency_ms = int ((time .time () - start_time ) * 1000 )
583601
584602 # Determine overall status
585603 # IMPORTANT: Ensure is_valid is only True when there are NO validation errors
586604 # This is a safeguard to prevent inconsistencies in the workflow
587605 is_valid = final_state ["is_valid" ] and len (final_state ["validation_errors" ]) == 0
588606 status = "success" if is_valid else "failed"
589607
608+ # Collect telemetry if enabled
609+ if request .telemetry_enabled and telemetry_collector :
610+ # Get model info from request body, BYOK headers, or server config
611+ model_name = (
612+ request .model
613+ or req .headers .get ("x-openrouter-model" )
614+ or os .getenv ("ANNOTATION_MODEL" , "openai/gpt-oss-120b" )
615+ )
616+ temperature = (
617+ request .temperature
618+ or float (req .headers .get ("x-openrouter-temperature" , 0 ))
619+ or _byok_config .get ("temperature" , 0.1 )
620+ )
621+
622+ event = TelemetryEvent .create (
623+ description = request .description ,
624+ schema_version = request .schema_version ,
625+ hed_string = final_state ["current_annotation" ],
626+ iterations = final_state ["validation_attempts" ],
627+ validation_errors = final_state ["validation_errors" ],
628+ model = model_name ,
629+ provider = request .provider or req .headers .get ("x-openrouter-provider" ),
630+ temperature = temperature ,
631+ latency_ms = latency_ms ,
632+ source = "api" ,
633+ )
634+ await telemetry_collector .collect (event )
635+
590636 return AnnotationResponse (
591637 annotation = final_state ["current_annotation" ],
592638 is_valid = is_valid ,
@@ -682,6 +728,8 @@ async def annotate_from_image(
682728 active_vision_agent = vision_agent
683729
684730 try :
731+ start_time = time .time ()
732+
685733 # Step 1: Generate image description using vision model
686734 vision_result = await active_vision_agent .describe_image (
687735 image_data = request .image ,
@@ -701,11 +749,40 @@ async def annotate_from_image(
701749 run_assessment = request .run_assessment ,
702750 config = config ,
703751 )
752+ latency_ms = int ((time .time () - start_time ) * 1000 )
704753
705754 # Determine overall status
706755 is_valid = final_state ["is_valid" ] and len (final_state ["validation_errors" ]) == 0
707756 status = "success" if is_valid else "failed"
708757
758+ # Collect telemetry if enabled
759+ if request .telemetry_enabled and telemetry_collector :
760+ # Get model info from request body, BYOK headers, or server config
761+ model_name = (
762+ request .model
763+ or req .headers .get ("x-openrouter-model" )
764+ or os .getenv ("ANNOTATION_MODEL" , "openai/gpt-oss-120b" )
765+ )
766+ temperature = (
767+ request .temperature
768+ or float (req .headers .get ("x-openrouter-temperature" , 0 ))
769+ or _byok_config .get ("temperature" , 0.1 )
770+ )
771+
772+ event = TelemetryEvent .create (
773+ description = image_description , # Use generated image description
774+ schema_version = request .schema_version ,
775+ hed_string = final_state ["current_annotation" ],
776+ iterations = final_state ["validation_attempts" ],
777+ validation_errors = final_state ["validation_errors" ],
778+ model = model_name ,
779+ provider = request .provider or req .headers .get ("x-openrouter-provider" ),
780+ temperature = temperature ,
781+ latency_ms = latency_ms ,
782+ source = "api-image" , # Distinguish from text-based annotation
783+ )
784+ await telemetry_collector .collect (event )
785+
709786 return ImageAnnotationResponse (
710787 image_description = image_description ,
711788 annotation = final_state ["current_annotation" ],
0 commit comments