-
Notifications
You must be signed in to change notification settings - Fork 469
fix(openai): async pagination for OpenAI list methods #14911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(openai): async pagination for OpenAI list methods #14911
Conversation
|
|
Bootstrap import analysisComparison of import times between this PR and base. SummaryThe average import time from this PR is: 239 ± 1 ms. The average import time from base is: 241 ± 2 ms. The import time difference between this PR and base is: -2.32 ± 0.07 ms. Import time breakdownThe following import paths have shrunk:
|
Performance SLOsComparing candidate alex/MLOB-4193_fix-openai-model-listing-function (ae07e83) with baseline main (92937df) 📈 Performance Regressions (2 suites)📈 iastaspectsospath - 24/24✅ ospathbasename_aspectTime: ✅ 5.124µs (SLO: <10.000µs 📉 -48.8%) vs baseline: 📈 +18.0% Memory: ✅ 37.709MB (SLO: <39.000MB -3.3%) vs baseline: +4.9% ✅ ospathbasename_noaspectTime: ✅ 1.099µs (SLO: <10.000µs 📉 -89.0%) vs baseline: +0.4% Memory: ✅ 37.670MB (SLO: <39.000MB -3.4%) vs baseline: +4.8% ✅ ospathjoin_aspectTime: ✅ 6.105µs (SLO: <10.000µs 📉 -38.9%) vs baseline: -0.6% Memory: ✅ 37.690MB (SLO: <39.000MB -3.4%) vs baseline: +5.0% ✅ ospathjoin_noaspectTime: ✅ 2.316µs (SLO: <10.000µs 📉 -76.8%) vs baseline: +0.4% Memory: ✅ 37.670MB (SLO: <39.000MB -3.4%) vs baseline: +4.9% ✅ ospathnormcase_aspectTime: ✅ 3.494µs (SLO: <10.000µs 📉 -65.1%) vs baseline: ~same Memory: ✅ 37.729MB (SLO: <39.000MB -3.3%) vs baseline: +5.2% ✅ ospathnormcase_noaspectTime: ✅ 0.577µs (SLO: <10.000µs 📉 -94.2%) vs baseline: +0.7% Memory: ✅ 37.690MB (SLO: <39.000MB -3.4%) vs baseline: +5.0% ✅ ospathsplit_aspectTime: ✅ 4.901µs (SLO: <10.000µs 📉 -51.0%) vs baseline: +0.8% Memory: ✅ 37.690MB (SLO: <39.000MB -3.4%) vs baseline: +4.8% ✅ ospathsplit_noaspectTime: ✅ 1.607µs (SLO: <10.000µs 📉 -83.9%) vs baseline: -0.2% Memory: ✅ 37.690MB (SLO: <39.000MB -3.4%) vs baseline: +4.9% ✅ ospathsplitdrive_aspectTime: ✅ 3.685µs (SLO: <10.000µs 📉 -63.1%) vs baseline: +1.0% Memory: ✅ 37.690MB (SLO: <39.000MB -3.4%) vs baseline: +4.7% ✅ ospathsplitdrive_noaspectTime: ✅ 0.703µs (SLO: <10.000µs 📉 -93.0%) vs baseline: -0.2% Memory: ✅ 37.650MB (SLO: <39.000MB -3.5%) vs baseline: +4.9% ✅ ospathsplitext_aspectTime: ✅ 4.610µs (SLO: <10.000µs 📉 -53.9%) vs baseline: +0.6% Memory: ✅ 37.690MB (SLO: <39.000MB -3.4%) vs baseline: +4.9% ✅ ospathsplitext_noaspectTime: ✅ 1.391µs (SLO: <10.000µs 📉 -86.1%) vs baseline: -0.7% Memory: ✅ 37.670MB (SLO: <39.000MB -3.4%) vs baseline: +4.9% 📈 telemetryaddmetric - 30/30✅ 1-count-metric-1-timesTime: ✅ 3.312µs (SLO: <20.000µs 📉 -83.4%) vs baseline: +5.1% Memory: ✅ 32.086MB (SLO: <34.000MB -5.6%) vs baseline: +4.8% ✅ 1-count-metrics-100-timesTime: ✅ 211.662µs (SLO: <250.000µs 📉 -15.3%) vs baseline: -1.4% Memory: ✅ 32.126MB (SLO: <34.000MB -5.5%) vs baseline: +4.9% ✅ 1-distribution-metric-1-timesTime: ✅ 3.192µs (SLO: <20.000µs 📉 -84.0%) vs baseline: +7.7% Memory: ✅ 32.145MB (SLO: <34.000MB -5.5%) vs baseline: +4.9% ✅ 1-distribution-metrics-100-timesTime: ✅ 190.816µs (SLO: <220.000µs 📉 -13.3%) vs baseline: -0.5% Memory: ✅ 32.145MB (SLO: <34.000MB -5.5%) vs baseline: +5.1% ✅ 1-gauge-metric-1-timesTime: ✅ 2.059µs (SLO: <20.000µs 📉 -89.7%) vs baseline: -0.6% Memory: ✅ 32.185MB (SLO: <34.000MB -5.3%) vs baseline: +4.9% ✅ 1-gauge-metrics-100-timesTime: ✅ 124.435µs (SLO: <150.000µs 📉 -17.0%) vs baseline: +0.6% Memory: ✅ 32.126MB (SLO: <34.000MB -5.5%) vs baseline: +4.9% ✅ 1-rate-metric-1-timesTime: ✅ 3.383µs (SLO: <20.000µs 📉 -83.1%) vs baseline: +7.0% Memory: ✅ 32.165MB (SLO: <34.000MB -5.4%) vs baseline: +4.9% ✅ 1-rate-metrics-100-timesTime: ✅ 212.554µs (SLO: <250.000µs 📉 -15.0%) vs baseline: -0.3% Memory: ✅ 32.106MB (SLO: <34.000MB -5.6%) vs baseline: +4.9% ✅ 100-count-metrics-100-timesTime: ✅ 21.740ms (SLO: <23.500ms -7.5%) vs baseline: +1.3% Memory: ✅ 32.185MB (SLO: <34.000MB -5.3%) vs baseline: +5.2% ✅ 100-distribution-metrics-100-timesTime: ✅ 1.970ms (SLO: <2.250ms 📉 -12.4%) vs baseline: -0.6% Memory: ✅ 32.086MB (SLO: <34.000MB -5.6%) vs baseline: +4.7% ✅ 100-gauge-metrics-100-timesTime: ✅ 1.282ms (SLO: <1.550ms 📉 -17.3%) vs baseline: ~same Memory: ✅ 32.086MB (SLO: <34.000MB -5.6%) vs baseline: +4.6% ✅ 100-rate-metrics-100-timesTime: ✅ 2.235ms (SLO: <2.550ms 📉 -12.4%) vs baseline: +1.2% Memory: ✅ 32.126MB (SLO: <34.000MB -5.5%) vs baseline: +4.9% ✅ flush-1-metricTime: ✅ 4.751µs (SLO: <20.000µs 📉 -76.2%) vs baseline: 📈 +12.4% Memory: ✅ 32.106MB (SLO: <34.000MB -5.6%) vs baseline: +4.9% ✅ flush-100-metricsTime: ✅ 180.552µs (SLO: <250.000µs 📉 -27.8%) vs baseline: -1.2% Memory: ✅ 32.165MB (SLO: <34.000MB -5.4%) vs baseline: +5.2% ✅ flush-1000-metricsTime: ✅ 2.229ms (SLO: <2.500ms 📉 -10.8%) vs baseline: +0.3% Memory: ✅ 32.873MB (SLO: <34.500MB -4.7%) vs baseline: +4.9% 🟡 Near SLO Breach (4 suites)🟡 djangosimple - 30/30✅ appsecTime: ✅ 20.475ms (SLO: <22.300ms -8.2%) vs baseline: +0.1% Memory: ✅ 65.458MB (SLO: <67.000MB -2.3%) vs baseline: +4.7% ✅ exception-replay-enabledTime: ✅ 1.348ms (SLO: <1.450ms -7.0%) vs baseline: +0.1% Memory: ✅ 64.546MB (SLO: <67.000MB -3.7%) vs baseline: +4.8% ✅ iastTime: ✅ 20.408ms (SLO: <22.250ms -8.3%) vs baseline: -0.2% Memory: ✅ 65.475MB (SLO: <67.000MB -2.3%) vs baseline: +4.9% ✅ profilerTime: ✅ 15.235ms (SLO: <16.550ms -7.9%) vs baseline: ~same Memory: ✅ 53.669MB (SLO: <54.500MB 🟡 -1.5%) vs baseline: +4.9% ✅ resource-renamingTime: ✅ 20.508ms (SLO: <21.750ms -5.7%) vs baseline: -0.2% Memory: ✅ 65.399MB (SLO: <67.000MB -2.4%) vs baseline: +4.7% ✅ span-code-originTime: ✅ 26.147ms (SLO: <28.200ms -7.3%) vs baseline: -0.3% Memory: ✅ 67.605MB (SLO: <69.500MB -2.7%) vs baseline: +4.9% ✅ tracerTime: ✅ 20.458ms (SLO: <21.750ms -5.9%) vs baseline: +0.2% Memory: ✅ 65.481MB (SLO: <67.000MB -2.3%) vs baseline: +4.8% ✅ tracer-and-profilerTime: ✅ 22.078ms (SLO: <23.500ms -6.1%) vs baseline: +0.3% Memory: ✅ 66.688MB (SLO: <67.500MB 🟡 -1.2%) vs baseline: +5.0% ✅ tracer-dont-create-db-spansTime: ✅ 19.286ms (SLO: <21.500ms 📉 -10.3%) vs baseline: -0.1% Memory: ✅ 65.440MB (SLO: <66.000MB 🟡 -0.8%) vs baseline: +4.8% ✅ tracer-minimalTime: ✅ 16.644ms (SLO: <17.500ms -4.9%) vs baseline: ~same Memory: ✅ 65.375MB (SLO: <66.000MB 🟡 -0.9%) vs baseline: +4.7% ✅ tracer-nativeTime: ✅ 20.448ms (SLO: <21.750ms -6.0%) vs baseline: -0.4% Memory: ✅ 71.381MB (SLO: <72.500MB 🟡 -1.5%) vs baseline: +4.8% ✅ tracer-no-cachesTime: ✅ 18.357ms (SLO: <19.650ms -6.6%) vs baseline: -0.6% Memory: ✅ 65.493MB (SLO: <67.000MB -2.2%) vs baseline: +4.9% ✅ tracer-no-databasesTime: ✅ 18.757ms (SLO: <20.100ms -6.7%) vs baseline: -0.2% Memory: ✅ 65.349MB (SLO: <67.000MB -2.5%) vs baseline: +4.8% ✅ tracer-no-middlewareTime: ✅ 20.139ms (SLO: <21.500ms -6.3%) vs baseline: -0.3% Memory: ✅ 65.472MB (SLO: <67.000MB -2.3%) vs baseline: +5.0% ✅ tracer-no-templatesTime: ✅ 20.254ms (SLO: <22.000ms -7.9%) vs baseline: -0.2% Memory: ✅ 65.539MB (SLO: <67.000MB -2.2%) vs baseline: +4.9% 🟡 errortrackingdjangosimple - 6/6✅ errortracking-enabled-allTime: ✅ 18.276ms (SLO: <19.850ms -7.9%) vs baseline: +1.0% Memory: ✅ 65.235MB (SLO: <66.500MB 🟡 -1.9%) vs baseline: +4.9% ✅ errortracking-enabled-userTime: ✅ 18.167ms (SLO: <19.400ms -6.4%) vs baseline: +0.4% Memory: ✅ 65.274MB (SLO: <66.500MB 🟡 -1.8%) vs baseline: +5.0% ✅ tracer-enabledTime: ✅ 18.150ms (SLO: <19.450ms -6.7%) vs baseline: +0.3% Memory: ✅ 65.294MB (SLO: <66.500MB 🟡 -1.8%) vs baseline: +5.0% 🟡 flasksimple - 18/18✅ appsec-getTime: ✅ 4.580ms (SLO: <4.750ms -3.6%) vs baseline: +0.1% Memory: ✅ 62.030MB (SLO: <65.000MB -4.6%) vs baseline: +5.0% ✅ appsec-postTime: ✅ 6.588ms (SLO: <6.750ms -2.4%) vs baseline: -0.1% Memory: ✅ 61.971MB (SLO: <65.000MB -4.7%) vs baseline: +4.8% ✅ appsec-telemetryTime: ✅ 4.570ms (SLO: <4.750ms -3.8%) vs baseline: ~same Memory: ✅ 61.971MB (SLO: <65.000MB -4.7%) vs baseline: +4.8% ✅ debuggerTime: ✅ 1.855ms (SLO: <2.000ms -7.3%) vs baseline: ~same Memory: ✅ 45.475MB (SLO: <47.000MB -3.2%) vs baseline: +5.0% ✅ iast-getTime: ✅ 1.861ms (SLO: <2.000ms -6.9%) vs baseline: ~same Memory: ✅ 42.389MB (SLO: <49.000MB 📉 -13.5%) vs baseline: +5.1% ✅ profilerTime: ✅ 1.910ms (SLO: <2.100ms -9.1%) vs baseline: ~same Memory: ✅ 46.537MB (SLO: <47.000MB 🟡 -1.0%) vs baseline: +4.9% ✅ resource-renamingTime: ✅ 3.370ms (SLO: <3.650ms -7.7%) vs baseline: ~same Memory: ✅ 52.258MB (SLO: <53.500MB -2.3%) vs baseline: +4.8% ✅ tracerTime: ✅ 3.359ms (SLO: <3.650ms -8.0%) vs baseline: +0.3% Memory: ✅ 52.239MB (SLO: <53.500MB -2.4%) vs baseline: +4.9% ✅ tracer-nativeTime: ✅ 3.361ms (SLO: <3.650ms -7.9%) vs baseline: ~same Memory: ✅ 58.216MB (SLO: <60.000MB -3.0%) vs baseline: +5.1% 🟡 otelspan - 22/22✅ add-eventTime: ✅ 41.036ms (SLO: <47.150ms 📉 -13.0%) vs baseline: -0.2% Memory: ✅ 44.183MB (SLO: <47.000MB -6.0%) vs baseline: +5.0% ✅ add-metricsTime: ✅ 315.519ms (SLO: <344.800ms -8.5%) vs baseline: -1.3% Memory: ✅ 616.702MB (SLO: <630.000MB -2.1%) vs baseline: +5.0% ✅ add-tagsTime: ✅ 287.763ms (SLO: <314.000ms -8.4%) vs baseline: ~same Memory: ✅ 618.643MB (SLO: <630.000MB 🟡 -1.8%) vs baseline: +5.0% ✅ get-contextTime: ✅ 81.052ms (SLO: <92.350ms 📉 -12.2%) vs baseline: +0.3% Memory: ✅ 39.783MB (SLO: <46.500MB 📉 -14.4%) vs baseline: +5.0% ✅ is-recordingTime: ✅ 38.238ms (SLO: <44.500ms 📉 -14.1%) vs baseline: ~same Memory: ✅ 43.588MB (SLO: <47.500MB -8.2%) vs baseline: +4.8% ✅ record-exceptionTime: ✅ 59.366ms (SLO: <67.650ms 📉 -12.2%) vs baseline: +2.0% Memory: ✅ 40.050MB (SLO: <47.000MB 📉 -14.8%) vs baseline: +4.6% ✅ set-statusTime: ✅ 44.056ms (SLO: <50.400ms 📉 -12.6%) vs baseline: -0.4% Memory: ✅ 43.598MB (SLO: <47.000MB -7.2%) vs baseline: +4.9% ✅ startTime: ✅ 37.552ms (SLO: <43.450ms 📉 -13.6%) vs baseline: ~same Memory: ✅ 43.527MB (SLO: <47.000MB -7.4%) vs baseline: +4.7% ✅ start-finishTime: ✅ 82.662ms (SLO: <88.000ms -6.1%) vs baseline: +0.2% Memory: ✅ 34.544MB (SLO: <46.500MB 📉 -25.7%) vs baseline: +4.9% ✅ start-finish-telemetryTime: ✅ 84.169ms (SLO: <89.000ms -5.4%) vs baseline: +0.4% Memory: ✅ 34.505MB (SLO: <46.500MB 📉 -25.8%) vs baseline: +4.7% ✅ update-nameTime: ✅ 39.251ms (SLO: <45.150ms 📉 -13.1%) vs baseline: ~same Memory: ✅ 43.966MB (SLO: <47.000MB -6.5%) vs baseline: +4.8%
|
brettlangdon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
release note lgtm
Hey, so this is an attempt to fix #14574 where doing `async for model in client.models.list()` would fail with `TypeError: 'async for' requires an object with __aiter__ method, got coroutine`. ### The Problem Methods like `AsyncModels.list()` and `AsyncFiles.list()` don't actually return coroutines - they return `AsyncPaginator` objects that you can either: - `await` to get the first page (what existing code does) - Use with `async for` to iterate through all items (what was broken) But our wrapper in `_patched_endpoint_async` was converting everything into coroutines, which broke the `async for` use case. ### What I Tried First attempt was using `inspect.iscoroutinefunction()` to detect which methods are actually async vs just returning async objects. That got messy fast because checking unbound methods from classes didn't work reliably. Then I tried just using the sync wrapper for list methods: ```python if method_name == "list": wrap(openai, async_method, _patched_endpoint(openai, endpoint_hook)) ``` This looked promising - the pagination tests passed! But it broke `test_model_alist` and `test_file_alist` because those tests do `await client.models.list()` and expect full tracing with response metadata like `openai.response.count`. Using the sync wrapper meant we lost all that when the paginator was awaited. Also tried returning the paginator directly without any wrapping, but that meant we lost tracing entirely when someone did `async for`. Not acceptable. ### A Solution Created a `_TracedAsyncPaginator` wrapper class that implements both `__aiter__` and `__await__`. This way: - When you do `await client.models.list()` -> calls `__await__`, traces properly, returns first page (existing behavior preserved) - When you do `async for model in client.models.list()` -> calls `__aiter__`, traces on first iteration, yields items (fixes the bug) The wrapper is ~50 lines but it's the minimal solution that preserves 100% backward compatibility while fixing the breaking bug. Had to use `finally` blocks to ensure traces complete even if iteration stops early. ### Testing Added two new pagination tests (`test_model_list_pagination` and `test_model_alist_pagination`) that specifically test the `async for` pattern. (cherry picked from commit 52929aa)
Hey, so this is an attempt to fix #14574 where doing `async for model in client.models.list()` would fail with `TypeError: 'async for' requires an object with __aiter__ method, got coroutine`. Methods like `AsyncModels.list()` and `AsyncFiles.list()` don't actually return coroutines - they return `AsyncPaginator` objects that you can either: - `await` to get the first page (what existing code does) - Use with `async for` to iterate through all items (what was broken) But our wrapper in `_patched_endpoint_async` was converting everything into coroutines, which broke the `async for` use case. First attempt was using `inspect.iscoroutinefunction()` to detect which methods are actually async vs just returning async objects. That got messy fast because checking unbound methods from classes didn't work reliably. Then I tried just using the sync wrapper for list methods: ```python if method_name == "list": wrap(openai, async_method, _patched_endpoint(openai, endpoint_hook)) ``` This looked promising - the pagination tests passed! But it broke `test_model_alist` and `test_file_alist` because those tests do `await client.models.list()` and expect full tracing with response metadata like `openai.response.count`. Using the sync wrapper meant we lost all that when the paginator was awaited. Also tried returning the paginator directly without any wrapping, but that meant we lost tracing entirely when someone did `async for`. Not acceptable. Created a `_TracedAsyncPaginator` wrapper class that implements both `__aiter__` and `__await__`. This way: - When you do `await client.models.list()` -> calls `__await__`, traces properly, returns first page (existing behavior preserved) - When you do `async for model in client.models.list()` -> calls `__aiter__`, traces on first iteration, yields items (fixes the bug) The wrapper is ~50 lines but it's the minimal solution that preserves 100% backward compatibility while fixing the breaking bug. Had to use `finally` blocks to ensure traces complete even if iteration stops early. Added two new pagination tests (`test_model_list_pagination` and `test_model_alist_pagination`) that specifically test the `async for` pattern. (cherry picked from commit 52929aa)
…#15067) Backport 52929aa from #14911 to 3.17. Hey, so this is an attempt to fix #14574 where doing `async for model in client.models.list()` would fail with `TypeError: 'async for' requires an object with __aiter__ method, got coroutine`. ### The Problem Methods like `AsyncModels.list()` and `AsyncFiles.list()` don't actually return coroutines - they return `AsyncPaginator` objects that you can either: - `await` to get the first page (what existing code does) - Use with `async for` to iterate through all items (what was broken) But our wrapper in `_patched_endpoint_async` was converting everything into coroutines, which broke the `async for` use case. ### What I Tried First attempt was using `inspect.iscoroutinefunction()` to detect which methods are actually async vs just returning async objects. That got messy fast because checking unbound methods from classes didn't work reliably. Then I tried just using the sync wrapper for list methods: ```python if method_name == "list": wrap(openai, async_method, _patched_endpoint(openai, endpoint_hook)) ``` This looked promising - the pagination tests passed! But it broke `test_model_alist` and `test_file_alist` because those tests do `await client.models.list()` and expect full tracing with response metadata like `openai.response.count`. Using the sync wrapper meant we lost all that when the paginator was awaited. Also tried returning the paginator directly without any wrapping, but that meant we lost tracing entirely when someone did `async for`. Not acceptable. ### A Solution Created a `_TracedAsyncPaginator` wrapper class that implements both `__aiter__` and `__await__`. This way: - When you do `await client.models.list()` -> calls `__await__`, traces properly, returns first page (existing behavior preserved) - When you do `async for model in client.models.list()` -> calls `__aiter__`, traces on first iteration, yields items (fixes the bug) The wrapper is ~50 lines but it's the minimal solution that preserves 100% backward compatibility while fixing the breaking bug. Had to use `finally` blocks to ensure traces complete even if iteration stops early. ### Testing Added two new pagination tests (`test_model_list_pagination` and `test_model_alist_pagination`) that specifically test the `async for` pattern. Co-authored-by: Alexandre Choura <[email protected]>
Hey, so this is an attempt to fix #14574 where doing `async for model in client.models.list()` would fail with `TypeError: 'async for' requires an object with __aiter__ method, got coroutine`. ### The Problem Methods like `AsyncModels.list()` and `AsyncFiles.list()` don't actually return coroutines - they return `AsyncPaginator` objects that you can either: - `await` to get the first page (what existing code does) - Use with `async for` to iterate through all items (what was broken) But our wrapper in `_patched_endpoint_async` was converting everything into coroutines, which broke the `async for` use case. ### What I Tried First attempt was using `inspect.iscoroutinefunction()` to detect which methods are actually async vs just returning async objects. That got messy fast because checking unbound methods from classes didn't work reliably. Then I tried just using the sync wrapper for list methods: ```python if method_name == "list": wrap(openai, async_method, _patched_endpoint(openai, endpoint_hook)) ``` This looked promising - the pagination tests passed! But it broke `test_model_alist` and `test_file_alist` because those tests do `await client.models.list()` and expect full tracing with response metadata like `openai.response.count`. Using the sync wrapper meant we lost all that when the paginator was awaited. Also tried returning the paginator directly without any wrapping, but that meant we lost tracing entirely when someone did `async for`. Not acceptable. ### A Solution Created a `_TracedAsyncPaginator` wrapper class that implements both `__aiter__` and `__await__`. This way: - When you do `await client.models.list()` -> calls `__await__`, traces properly, returns first page (existing behavior preserved) - When you do `async for model in client.models.list()` -> calls `__aiter__`, traces on first iteration, yields items (fixes the bug) The wrapper is ~50 lines but it's the minimal solution that preserves 100% backward compatibility while fixing the breaking bug. Had to use `finally` blocks to ensure traces complete even if iteration stops early. ### Testing Added two new pagination tests (`test_model_list_pagination` and `test_model_alist_pagination`) that specifically test the `async for` pattern.
Hey, so this is an attempt to fix #14574 where doing
async for model in client.models.list()would fail withTypeError: 'async for' requires an object with __aiter__ method, got coroutine.The Problem
Methods like
AsyncModels.list()andAsyncFiles.list()don't actually return coroutines - they returnAsyncPaginatorobjects that you can either:awaitto get the first page (what existing code does)async forto iterate through all items (what was broken)But our wrapper in
_patched_endpoint_asyncwas converting everything into coroutines, which broke theasync foruse case.What I Tried
First attempt was using
inspect.iscoroutinefunction()to detect which methods are actually async vs just returning async objects. That got messy fast because checking unbound methods from classes didn't work reliably.Then I tried just using the sync wrapper for list methods:
This looked promising - the pagination tests passed! But it broke
test_model_alistandtest_file_alistbecause those tests doawait client.models.list()and expect full tracing with response metadata likeopenai.response.count. Using the sync wrapper meant we lost all that when the paginator was awaited.Also tried returning the paginator directly without any wrapping, but that meant we lost tracing entirely when someone did
async for. Not acceptable.A Solution
Created a
_TracedAsyncPaginatorwrapper class that implements both__aiter__and__await__. This way:await client.models.list()-> calls__await__, traces properly, returns first page (existing behavior preserved)async for model in client.models.list()-> calls__aiter__, traces on first iteration, yields items (fixes the bug)The wrapper is ~50 lines but it's the minimal solution that preserves 100% backward compatibility while fixing the breaking bug. Had to use
finallyblocks to ensure traces complete even if iteration stops early.Testing
Added two new pagination tests (
test_model_list_paginationandtest_model_alist_pagination) that specifically test theasync forpattern.