@@ -48,6 +48,7 @@ class TableState(TypedDict, total=False):
4848 errors : List [str ]
4949 synthetic_json : dict # 파싱된 합성 데이터 JSON
5050 qa_results : List [Dict ] # 생성된 QA 쌍
51+ token_usage : int # QA 생성에 사용된 총 토큰 수
5152
5253
5354def _encode_image (image_path : Path ) -> str :
@@ -67,8 +68,18 @@ def _encode_image(image_path: Path) -> str:
6768
6869
6970def _call_llm (
70- llm : ChatOpenAI , prompt : str , image_urls : Optional [List [str ]] = None ) -> str :
71- """Call the multi-modal LLM with optional multiple images."""
71+ llm : ChatOpenAI , prompt : str , image_urls : Optional [List [str ]] = None , return_token_usage : bool = False ) -> str :
72+ """Call the multi-modal LLM with optional multiple images.
73+
74+ Args:
75+ llm: The LLM instance
76+ prompt: The text prompt
77+ image_urls: Optional list of image URLs
78+ return_token_usage: If True, returns tuple of (content, total_tokens)
79+
80+ Returns:
81+ Response content string, or tuple of (content, total_tokens) if return_token_usage=True
82+ """
7283
7384 content : List [Dict ] = [{"type" : "text" , "text" : prompt }]
7485
@@ -83,7 +94,31 @@ def _call_llm(
8394 })
8495
8596 response = llm .invoke ([HumanMessage (content = content )])
86- return response .content if isinstance (response .content , str ) else json .dumps (response .content )
97+ response_content = response .content if isinstance (response .content , str ) else json .dumps (response .content )
98+
99+ if return_token_usage :
100+ # Extract token usage from response metadata
101+ token_usage = 0
102+ if hasattr (response , 'response_metadata' ):
103+ usage_metadata = response .response_metadata .get ('usage' , {})
104+ # OpenAI/Gemini format
105+ token_usage = usage_metadata .get ('total_tokens' , 0 )
106+ # Fallback: prompt_tokens + completion_tokens
107+ if not token_usage :
108+ token_usage = usage_metadata .get ('prompt_tokens' , 0 ) + usage_metadata .get ('completion_tokens' , 0 )
109+ # Fallback: input_tokens + output_tokens
110+ if not token_usage :
111+ token_usage = usage_metadata .get ('input_tokens' , 0 ) + usage_metadata .get ('output_tokens' , 0 )
112+ # Alternative: usage_metadata attribute (dict or object)
113+ if not token_usage and hasattr (response , 'usage_metadata' ) and response .usage_metadata :
114+ usage = response .usage_metadata
115+ if isinstance (usage , dict ):
116+ token_usage = usage .get ('total_tokens' , 0 ) or (usage .get ('input_tokens' , 0 ) + usage .get ('output_tokens' , 0 ))
117+ else :
118+ token_usage = getattr (usage , 'total_tokens' , 0 ) or (getattr (usage , 'input_tokens' , 0 ) + getattr (usage , 'output_tokens' , 0 ))
119+ return response_content , token_usage
120+
121+ return response_content
87122
88123
89124def _load_yaml_prompts (filename : str ) -> Dict [str , str ]:
@@ -649,7 +684,7 @@ def _node(state: TableState) -> TableState:
649684
650685 prompt = prompt_template
651686
652- response_text = _call_llm (llm , prompt , image_urls = image_data_urls )
687+ response_text , token_usage = _call_llm (llm , prompt , image_urls = image_data_urls , return_token_usage = True )
653688 response_json = robust_json_parse (response_text )
654689
655690 qa_results = []
@@ -658,7 +693,7 @@ def _node(state: TableState) -> TableState:
658693 else :
659694 logger .warning ("QA generation from image did not return valid JSON or 'qa_pairs' key." )
660695
661- return {** state , "qa_results" : qa_results }
696+ return {** state , "qa_results" : qa_results , "token_usage" : token_usage }
662697
663698 return _node
664699
0 commit comments