@@ -36,6 +36,7 @@ class TableState(TypedDict, total=False):
3636 errors : List [str ]
3737 synthetic_json : dict # 파싱된 합성 데이터 JSON
3838 qa_results : List [Dict ] # 생성된 QA 쌍
39+ token_usage : int # QA 생성에 사용된 총 토큰 수
3940
4041
4142def _encode_image (image_path : Path ) -> str :
@@ -55,8 +56,18 @@ def _encode_image(image_path: Path) -> str:
5556
5657
5758def _call_llm (
58- llm : ChatOpenAI , prompt : str , image_urls : Optional [List [str ]] = None ) -> str :
59- """Call the multi-modal LLM with optional multiple images."""
59+ llm : ChatOpenAI , prompt : str , image_urls : Optional [List [str ]] = None , return_token_usage : bool = False ) -> str :
60+ """Call the multi-modal LLM with optional multiple images.
61+
62+ Args:
63+ llm: The LLM instance
64+ prompt: The text prompt
65+ image_urls: Optional list of image URLs
66+ return_token_usage: If True, returns tuple of (content, total_tokens)
67+
68+ Returns:
69+ Response content string, or tuple of (content, total_tokens) if return_token_usage=True
70+ """
6071
6172 content : List [Dict ] = [{"type" : "text" , "text" : prompt }]
6273
@@ -71,7 +82,31 @@ def _call_llm(
7182 })
7283
7384 response = llm .invoke ([HumanMessage (content = content )])
74- return response .content if isinstance (response .content , str ) else json .dumps (response .content )
85+ response_content = response .content if isinstance (response .content , str ) else json .dumps (response .content )
86+
87+ if return_token_usage :
88+ # Extract token usage from response metadata
89+ token_usage = 0
90+ if hasattr (response , 'response_metadata' ):
91+ usage_metadata = response .response_metadata .get ('usage' , {})
92+ # OpenAI/Gemini format
93+ token_usage = usage_metadata .get ('total_tokens' , 0 )
94+ # Fallback: prompt_tokens + completion_tokens
95+ if not token_usage :
96+ token_usage = usage_metadata .get ('prompt_tokens' , 0 ) + usage_metadata .get ('completion_tokens' , 0 )
97+ # Fallback: input_tokens + output_tokens
98+ if not token_usage :
99+ token_usage = usage_metadata .get ('input_tokens' , 0 ) + usage_metadata .get ('output_tokens' , 0 )
100+ # Alternative: usage_metadata attribute (dict or object)
101+ if not token_usage and hasattr (response , 'usage_metadata' ) and response .usage_metadata :
102+ usage = response .usage_metadata
103+ if isinstance (usage , dict ):
104+ token_usage = usage .get ('total_tokens' , 0 ) or (usage .get ('input_tokens' , 0 ) + usage .get ('output_tokens' , 0 ))
105+ else :
106+ token_usage = getattr (usage , 'total_tokens' , 0 ) or (getattr (usage , 'input_tokens' , 0 ) + getattr (usage , 'output_tokens' , 0 ))
107+ return response_content , token_usage
108+
109+ return response_content
75110
76111
77112def _load_yaml_prompts (filename : str ) -> Dict [str , str ]:
@@ -566,7 +601,7 @@ def _node(state: TableState) -> TableState:
566601
567602 prompt = prompt_template
568603
569- response_text = _call_llm (llm , prompt , image_urls = image_data_urls )
604+ response_text , token_usage = _call_llm (llm , prompt , image_urls = image_data_urls , return_token_usage = True )
570605 response_json = robust_json_parse (response_text )
571606
572607 qa_results = []
@@ -575,7 +610,7 @@ def _node(state: TableState) -> TableState:
575610 else :
576611 logger .warning ("QA generation from image did not return valid JSON or 'qa_pairs' key." )
577612
578- return {** state , "qa_results" : qa_results }
613+ return {** state , "qa_results" : qa_results , "token_usage" : token_usage }
579614
580615 return _node
581616
0 commit comments