@@ -113,6 +113,7 @@ def test_metadata_capture(mock_client):
113113 base_url = "https://us.posthog.com" ,
114114 name = "test" ,
115115 end_time = None ,
116+ posthog_properties = None ,
116117 )
117118 assert callbacks ._runs [run_id ] == expected
118119 with patch ("time.time" , return_value = 1234567891 ):
@@ -1269,6 +1270,7 @@ def test_metadata_tools(mock_client):
12691270 name = "test" ,
12701271 tools = tools ,
12711272 end_time = None ,
1273+ posthog_properties = None ,
12721274 )
12731275 assert callbacks ._runs [run_id ] == expected
12741276 with patch ("time.time" , return_value = 1234567891 ):
@@ -2171,3 +2173,180 @@ def test_agent_action_and_finish_imports():
21712173 assert mock_client .capture .call_count == 1
21722174 call_args = mock_client .capture .call_args [1 ]
21732175 assert call_args ["event" ] == "$ai_span"
2176+
2177+
2178+ def test_posthog_properties_field_in_generation_metadata (mock_client ):
2179+ """Test that posthog_properties is properly stored in GenerationMetadata."""
2180+ callbacks = CallbackHandler (mock_client )
2181+ run_id = uuid .uuid4 ()
2182+
2183+ # Test with billable=True
2184+ with patch ("time.time" , return_value = 1234567890 ):
2185+ callbacks ._set_llm_metadata (
2186+ {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2187+ run_id ,
2188+ messages = [{"role" : "user" , "content" : "Test message" }],
2189+ invocation_params = {"temperature" : 0.5 },
2190+ metadata = {
2191+ "ls_model_name" : "gpt-4o" ,
2192+ "ls_provider" : "openai" ,
2193+ "posthog_properties" : {"$ai_billable" : True },
2194+ },
2195+ name = "test" ,
2196+ )
2197+
2198+ expected = GenerationMetadata (
2199+ model = "gpt-4o" ,
2200+ input = [{"role" : "user" , "content" : "Test message" }],
2201+ start_time = 1234567890 ,
2202+ model_params = {"temperature" : 0.5 },
2203+ provider = "openai" ,
2204+ base_url = "https://api.openai.com" ,
2205+ name = "test" ,
2206+ posthog_properties = {"$ai_billable" : True },
2207+ end_time = None ,
2208+ )
2209+ assert callbacks ._runs [run_id ] == expected
2210+ assert callbacks ._runs [run_id ].posthog_properties == {"$ai_billable" : True }
2211+
2212+ callbacks ._pop_run_metadata (run_id )
2213+
2214+ # Test with billable=False (explicit)
2215+ run_id2 = uuid .uuid4 ()
2216+ with patch ("time.time" , return_value = 1234567890 ):
2217+ callbacks ._set_llm_metadata (
2218+ {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2219+ run_id2 ,
2220+ messages = [{"role" : "user" , "content" : "Test message" }],
2221+ invocation_params = {"temperature" : 0.5 },
2222+ metadata = {
2223+ "ls_model_name" : "gpt-4o" ,
2224+ "ls_provider" : "openai" ,
2225+ "posthog_properties" : {"$ai_billable" : False },
2226+ },
2227+ name = "test" ,
2228+ )
2229+
2230+ assert callbacks ._runs [run_id2 ].posthog_properties == {"$ai_billable" : False }
2231+ callbacks ._pop_run_metadata (run_id2 )
2232+
2233+ # Test when posthog_properties not provided
2234+ run_id3 = uuid .uuid4 ()
2235+ with patch ("time.time" , return_value = 1234567890 ):
2236+ callbacks ._set_llm_metadata (
2237+ {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2238+ run_id3 ,
2239+ messages = [{"role" : "user" , "content" : "Test message" }],
2240+ invocation_params = {"temperature" : 0.5 },
2241+ metadata = {"ls_model_name" : "gpt-4o" , "ls_provider" : "openai" },
2242+ name = "test" ,
2243+ )
2244+
2245+ assert callbacks ._runs [run_id3 ].posthog_properties is None
2246+
2247+
2248+ def test_billable_property_in_generation_event (mock_client ):
2249+ """Test that the billable property is captured in the $ai_generation event."""
2250+ callbacks = CallbackHandler (mock_client )
2251+
2252+ # We need to test the _set_llm_metadata directly since FakeMessagesListChatModel
2253+ # doesn't support metadata in the same way as real models
2254+ run_id = uuid .uuid4 ()
2255+ with patch ("time.time" , return_value = 1234567890 ):
2256+ callbacks ._set_llm_metadata (
2257+ {},
2258+ run_id ,
2259+ messages = [{"role" : "user" , "content" : "Test" }],
2260+ metadata = {
2261+ "posthog_properties" : {"$ai_billable" : True },
2262+ "ls_model_name" : "test-model" ,
2263+ },
2264+ invocation_params = {},
2265+ )
2266+
2267+ mock_response = MagicMock ()
2268+ mock_response .generations = [[MagicMock ()]]
2269+
2270+ with patch ("time.time" , return_value = 1234567891 ):
2271+ run = callbacks ._pop_run_metadata (run_id )
2272+
2273+ callbacks ._capture_generation (
2274+ trace_id = run_id ,
2275+ run_id = run_id ,
2276+ run = run ,
2277+ output = mock_response ,
2278+ parent_run_id = None ,
2279+ )
2280+
2281+ assert mock_client .capture .call_count == 1
2282+ call_args = mock_client .capture .call_args [1 ]
2283+ props = call_args ["properties" ]
2284+
2285+ assert call_args ["event" ] == "$ai_generation"
2286+ assert props ["$ai_billable" ] is True
2287+
2288+
2289+ def test_billable_defaults_to_false_in_event (mock_client ):
2290+ """Test that $ai_billable is not present when not specified."""
2291+ prompt = ChatPromptTemplate .from_messages ([("user" , "Test query" )])
2292+ model = FakeMessagesListChatModel (
2293+ responses = [AIMessage (content = "Test response" )],
2294+ )
2295+
2296+ callbacks = [CallbackHandler (mock_client )]
2297+ chain = prompt | model
2298+ chain .invoke ({}, config = {"callbacks" : callbacks })
2299+
2300+ generation_call = None
2301+ for call in mock_client .capture .call_args_list :
2302+ if call [1 ]["event" ] == "$ai_generation" :
2303+ generation_call = call
2304+ break
2305+
2306+ assert generation_call is not None
2307+ props = generation_call [1 ]["properties" ]
2308+ assert "$ai_billable" not in props
2309+
2310+
2311+ def test_billable_with_real_chain (mock_client ):
2312+ """Test billable tracking through a complete chain execution with mocked metadata."""
2313+ callbacks = CallbackHandler (mock_client )
2314+ run_id = uuid .uuid4 ()
2315+
2316+ with patch ("time.time" , return_value = 1000.0 ):
2317+ callbacks ._set_llm_metadata (
2318+ {},
2319+ run_id ,
2320+ messages = [{"role" : "user" , "content" : "What's the weather?" }],
2321+ metadata = {
2322+ "ls_model_name" : "fake-model" ,
2323+ "ls_provider" : "fake" ,
2324+ "posthog_properties" : {"$ai_billable" : True },
2325+ },
2326+ invocation_params = {"temperature" : 0.7 },
2327+ )
2328+
2329+ assert callbacks ._runs [run_id ].posthog_properties == {"$ai_billable" : True }
2330+
2331+ mock_response = MagicMock ()
2332+ mock_response .generations = [[MagicMock ()]]
2333+
2334+ with patch ("time.time" , return_value = 1001.0 ):
2335+ run = callbacks ._pop_run_metadata (run_id )
2336+
2337+ callbacks ._capture_generation (
2338+ trace_id = run_id ,
2339+ run_id = run_id ,
2340+ run = run ,
2341+ output = mock_response ,
2342+ parent_run_id = None ,
2343+ )
2344+
2345+ assert mock_client .capture .call_count == 1
2346+ call_args = mock_client .capture .call_args [1 ]
2347+ props = call_args ["properties" ]
2348+
2349+ assert call_args ["event" ] == "$ai_generation"
2350+ assert props ["$ai_billable" ] is True
2351+ assert props ["$ai_model" ] == "fake-model"
2352+ assert props ["$ai_provider" ] == "fake"
0 commit comments