@@ -2126,3 +2126,177 @@ def test_agent_action_and_finish_imports():
21262126 assert mock_client .capture .call_count == 1
21272127 call_args = mock_client .capture .call_args [1 ]
21282128 assert call_args ["event" ] == "$ai_span"
2129+
2130+
2131+ def test_billable_field_in_generation_metadata (mock_client ):
2132+ """Test that the billable field is properly stored in GenerationMetadata."""
2133+ callbacks = CallbackHandler (mock_client )
2134+ run_id = uuid .uuid4 ()
2135+
2136+ # Test with billable=True
2137+ with patch ("time.time" , return_value = 1234567890 ):
2138+ callbacks ._set_llm_metadata (
2139+ {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2140+ run_id ,
2141+ messages = [{"role" : "user" , "content" : "Test message" }],
2142+ invocation_params = {"temperature" : 0.5 },
2143+ metadata = {
2144+ "ls_model_name" : "gpt-4o" ,
2145+ "ls_provider" : "openai" ,
2146+ "posthog_ai_billable" : True ,
2147+ },
2148+ name = "test" ,
2149+ )
2150+
2151+ expected = GenerationMetadata (
2152+ model = "gpt-4o" ,
2153+ input = [{"role" : "user" , "content" : "Test message" }],
2154+ start_time = 1234567890 ,
2155+ model_params = {"temperature" : 0.5 },
2156+ provider = "openai" ,
2157+ base_url = "https://api.openai.com" ,
2158+ name = "test" ,
2159+ billable = True ,
2160+ end_time = None ,
2161+ )
2162+ assert callbacks ._runs [run_id ] == expected
2163+ assert callbacks ._runs [run_id ].billable is True
2164+
2165+ callbacks ._pop_run_metadata (run_id )
2166+
2167+ # Test with billable=False (explicit)
2168+ run_id2 = uuid .uuid4 ()
2169+ with patch ("time.time" , return_value = 1234567890 ):
2170+ callbacks ._set_llm_metadata (
2171+ {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2172+ run_id2 ,
2173+ messages = [{"role" : "user" , "content" : "Test message" }],
2174+ invocation_params = {"temperature" : 0.5 },
2175+ metadata = {
2176+ "ls_model_name" : "gpt-4o" ,
2177+ "ls_provider" : "openai" ,
2178+ "posthog_ai_billable" : False ,
2179+ },
2180+ name = "test" ,
2181+ )
2182+
2183+ assert callbacks ._runs [run_id2 ].billable is False
2184+ callbacks ._pop_run_metadata (run_id2 )
2185+
2186+ # Test default billable=False when not provided
2187+ run_id3 = uuid .uuid4 ()
2188+ with patch ("time.time" , return_value = 1234567890 ):
2189+ callbacks ._set_llm_metadata (
2190+ {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2191+ run_id3 ,
2192+ messages = [{"role" : "user" , "content" : "Test message" }],
2193+ invocation_params = {"temperature" : 0.5 },
2194+ metadata = {"ls_model_name" : "gpt-4o" , "ls_provider" : "openai" },
2195+ name = "test" ,
2196+ )
2197+
2198+ assert callbacks ._runs [run_id3 ].billable is False
2199+
2200+
2201+ def test_billable_property_in_generation_event (mock_client ):
2202+ """Test that the billable property is captured in the $ai_generation event."""
2203+ callbacks = CallbackHandler (mock_client )
2204+
2205+ # We need to test the _set_llm_metadata directly since FakeMessagesListChatModel
2206+ # doesn't support metadata in the same way as real models
2207+ run_id = uuid .uuid4 ()
2208+ with patch ("time.time" , return_value = 1234567890 ):
2209+ callbacks ._set_llm_metadata (
2210+ {},
2211+ run_id ,
2212+ messages = [{"role" : "user" , "content" : "Test" }],
2213+ metadata = {"posthog_ai_billable" : True , "ls_model_name" : "test-model" },
2214+ invocation_params = {},
2215+ )
2216+
2217+ mock_response = MagicMock ()
2218+ mock_response .generations = [[MagicMock ()]]
2219+
2220+ with patch ("time.time" , return_value = 1234567891 ):
2221+ run = callbacks ._pop_run_metadata (run_id )
2222+
2223+ callbacks ._capture_generation (
2224+ trace_id = run_id ,
2225+ run_id = run_id ,
2226+ run = run ,
2227+ output = mock_response ,
2228+ parent_run_id = None ,
2229+ )
2230+
2231+ assert mock_client .capture .call_count == 1
2232+ call_args = mock_client .capture .call_args [1 ]
2233+ props = call_args ["properties" ]
2234+
2235+ assert call_args ["event" ] == "$ai_generation"
2236+ assert props ["$ai_billable" ] is True
2237+
2238+
2239+ def test_billable_defaults_to_false_in_event (mock_client ):
2240+ """Test that $ai_billable defaults to False when not specified."""
2241+ prompt = ChatPromptTemplate .from_messages ([("user" , "Test query" )])
2242+ model = FakeMessagesListChatModel (
2243+ responses = [AIMessage (content = "Test response" )],
2244+ )
2245+
2246+ callbacks = [CallbackHandler (mock_client )]
2247+ chain = prompt | model
2248+ chain .invoke ({}, config = {"callbacks" : callbacks })
2249+
2250+ generation_call = None
2251+ for call in mock_client .capture .call_args_list :
2252+ if call [1 ]["event" ] == "$ai_generation" :
2253+ generation_call = call
2254+ break
2255+
2256+ assert generation_call is not None
2257+ props = generation_call [1 ]["properties" ]
2258+ assert props ["$ai_billable" ] is False
2259+
2260+
2261+ def test_billable_with_real_chain (mock_client ):
2262+ """Test billable tracking through a complete chain execution with mocked metadata."""
2263+ callbacks = CallbackHandler (mock_client )
2264+ run_id = uuid .uuid4 ()
2265+
2266+ with patch ("time.time" , return_value = 1000.0 ):
2267+ callbacks ._set_llm_metadata (
2268+ {},
2269+ run_id ,
2270+ messages = [{"role" : "user" , "content" : "What's the weather?" }],
2271+ metadata = {
2272+ "ls_model_name" : "fake-model" ,
2273+ "ls_provider" : "fake" ,
2274+ "posthog_ai_billable" : True ,
2275+ },
2276+ invocation_params = {"temperature" : 0.7 },
2277+ )
2278+
2279+ assert callbacks ._runs [run_id ].billable is True
2280+
2281+ mock_response = MagicMock ()
2282+ mock_response .generations = [[MagicMock ()]]
2283+
2284+ with patch ("time.time" , return_value = 1001.0 ):
2285+ run = callbacks ._pop_run_metadata (run_id )
2286+
2287+ callbacks ._capture_generation (
2288+ trace_id = run_id ,
2289+ run_id = run_id ,
2290+ run = run ,
2291+ output = mock_response ,
2292+ parent_run_id = None ,
2293+ )
2294+
2295+ assert mock_client .capture .call_count == 1
2296+ call_args = mock_client .capture .call_args [1 ]
2297+ props = call_args ["properties" ]
2298+
2299+ assert call_args ["event" ] == "$ai_generation"
2300+ assert props ["$ai_billable" ] is True
2301+ assert props ["$ai_model" ] == "fake-model"
2302+ assert props ["$ai_provider" ] == "fake"
0 commit comments