@@ -467,3 +467,105 @@ def df_func(df: pd.DataFrame) -> pd.DataFrame:
467467 gen = _create_test_generator (name = "result" , generator_function = df_func )
468468 with pytest .raises (CustomColumnGenerationError , match = "first parameter must be 'row', got 'df'" ):
469469 gen .generate ({"input" : 1 })
470+
471+
472+ # Async model bridge tests
473+
474+
475+ class TestAsyncBridgedModelFacade :
476+ """Tests for _AsyncBridgedModelFacade proxy used by custom columns with model access."""
477+
478+ def test_proxy_transparent_in_sync_mode (self , stub_resource_provider , stub_model_facade ) -> None :
479+ """Proxy passes through generate(), forwards attributes, and is used by _build_models_dict."""
480+ from data_designer .engine .column_generators .generators .custom import _AsyncBridgedModelFacade
481+
482+ @custom_column_generator (required_columns = ["input" ], model_aliases = ["test-model" ])
483+ def gen_with_model (row : dict , generator_params : SampleParams , models : dict ) -> dict :
484+ row ["result" ] = "ok"
485+ return row
486+
487+ generator = _create_test_generator (
488+ name = "result" ,
489+ generator_function = gen_with_model ,
490+ generator_params = SampleParams (),
491+ resource_provider = stub_resource_provider ,
492+ )
493+
494+ models = generator ._build_models_dict ()
495+ proxy = models ["test-model" ]
496+ assert isinstance (proxy , _AsyncBridgedModelFacade )
497+
498+ # generate() passes through to the underlying facade (positional and keyword args)
499+ result , _ = proxy .generate ("test" , parser = str )
500+ assert result == "Generated summary text"
501+ stub_model_facade .generate .assert_called_once_with ("test" , parser = str )
502+
503+ # Other attributes are forwarded
504+ assert proxy .model_alias == "test_model"
505+
506+ def test_bridges_to_agenerate_on_sync_client_error (self ) -> None :
507+ """When sync generate() fails with an async/sync error, falls back to agenerate()."""
508+ import asyncio
509+ import threading
510+ from unittest .mock import patch
511+
512+ from data_designer .engine .column_generators .generators .custom import _AsyncBridgedModelFacade
513+
514+ facade = Mock ()
515+ facade .generate .side_effect = RuntimeError ("Sync methods are not available on an async-mode HttpModelClient." )
516+
517+ async def fake_agenerate (* args : Any , ** kwargs : Any ) -> tuple :
518+ return ("async_result" , list (args ))
519+
520+ facade .agenerate = fake_agenerate
521+ proxy = _AsyncBridgedModelFacade (facade )
522+
523+ engine_loop = asyncio .new_event_loop ()
524+ engine_thread = threading .Thread (target = engine_loop .run_forever , daemon = True )
525+ engine_thread .start ()
526+
527+ try :
528+ with patch (
529+ "data_designer.engine.dataset_builders.utils.async_concurrency.ensure_async_engine_loop" ,
530+ return_value = engine_loop ,
531+ ):
532+ # Positional prompt arg is forwarded to agenerate
533+ result = proxy .generate ("hello" , parser = str )
534+ assert result == ("async_result" , ["hello" ])
535+ finally :
536+ engine_loop .call_soon_threadsafe (engine_loop .stop )
537+ engine_thread .join (timeout = 5 )
538+
539+ def test_non_client_mode_errors_propagate (self ) -> None :
540+ """Only the specific HttpModelClient sync-mode RuntimeError triggers bridging."""
541+ from data_designer .engine .column_generators .generators .custom import _AsyncBridgedModelFacade
542+
543+ # ValueError - different type entirely
544+ facade = Mock ()
545+ facade .generate .side_effect = ValueError ("invalid prompt format" )
546+ proxy = _AsyncBridgedModelFacade (facade )
547+ with pytest .raises (ValueError , match = "invalid prompt format" ):
548+ proxy .generate (prompt = "hello" )
549+
550+ # RuntimeError with a different message - same type, not caught
551+ facade = Mock ()
552+ facade .generate .side_effect = RuntimeError ("connection timed out for async request" )
553+ proxy = _AsyncBridgedModelFacade (facade )
554+ with pytest .raises (RuntimeError , match = "connection timed out" ):
555+ proxy .generate (prompt = "hello" )
556+
557+ def test_deadlock_guard_on_event_loop (self ) -> None :
558+ """Raises a clear error instead of deadlocking when called from the event loop."""
559+ import asyncio
560+
561+ from data_designer .engine .column_generators .generators .custom import _AsyncBridgedModelFacade
562+
563+ facade = Mock ()
564+ facade .generate .side_effect = RuntimeError ("Sync methods are not available on an async-mode HttpModelClient." )
565+ proxy = _AsyncBridgedModelFacade (facade )
566+
567+ async def call_from_loop () -> None :
568+ proxy .generate (prompt = "hello" )
569+
570+ with pytest .raises (RuntimeError , match = "Use 'await model.agenerate\\ (\\ )'" ):
571+ asyncio .run (call_from_loop ())
0 commit comments