Skip to content

fix(client): preserve SDK retry behavior in NemoClient adapters - #992

Open
maxdubrinsky wants to merge 1 commit into
mainfrom
aircore-876-nemoclient-retry-fix/md
Open

fix(client): preserve SDK retry behavior in NemoClient adapters#992
maxdubrinsky wants to merge 1 commit into
mainfrom
aircore-876-nemoclient-retry-fix/md

Conversation

@maxdubrinsky

@maxdubrinsky maxdubrinsky commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

Preserve the SDK's retry behavior in the generic NemoClient adapter layer. Split out of the AIRCORE-876 Models typed-client migration as an independent, low-risk client-layer change.

The generic adapter now preserves the SDK retry policy:

  • Stainless retry-decision headers are honored.
  • Retry-After headers are honored, with a fallback when the value is unreasonable.
  • The standalone (non-Stainless) policy keeps its narrower retryable status set (502, 503, 504, 429).

Why split

The AIRCORE-876 branch (aircore-876-migrate-models-to-nemoclient/md) is a large, vendoring-gated migration. This retry hardening is conceptually independent of the models work and benefits every NemoClient consumer, so it is carved off to shrink the review surface of the big PR. It can merge in any order relative to the typed-models-client PR (disjoint files).

Scope

Client layer only, no consumer behavior changes:

  • client/adapter.py, client/client.py, client/types.py
  • Tests: tests/client/test_adapter.py, tests/client/test_client_options.py

Verification

  • pytest packages/nemo_platform_plugin/tests -> 1042 passed
  • ruff check / ruff format --check clean

Summary by CodeRabbit

  • New Features

    • Expanded retry policy support to include retryable HTTP statuses and optional handling of server errors.
    • Added configurable behavior to honor retry-decision and Retry-After headers (including multiple delay formats with safe limits).
  • Bug Fixes

    • Improved retry timing using exponential backoff and consistent logic across synchronous and asynchronous clients, including correct Retry-After parsing and application.
  • Tests

    • Updated and strengthened retry policy coverage for defaults, header-driven behavior, and delay parsing edge cases.

@maxdubrinsky
maxdubrinsky requested review from a team as code owners July 30, 2026 17:39
@github-actions github-actions Bot added the fix label Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The retry policy now supports configurable server-error retries, retry-decision headers, and Retry-After delays. Platform adapter wiring uses these options, with synchronous and asynchronous tests covering status codes, headers, delay parsing, and defaults.

Changes

Retry policy behavior

Layer / File(s) Summary
RetryPolicy contract
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/types.py
Adds opt-in flags for server errors, retry-decision headers, and Retry-After headers, with updated documentation.
Retry evaluation and delay parsing
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py, packages/nemo_platform_plugin/tests/client/test_client_options.py
Parses numeric and HTTP-date retry delays, applies header-driven decisions and exponential backoff, enforces retry limits, and tests sync/async behavior.
Platform adapter policy wiring
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/adapter.py, packages/nemo_platform_plugin/tests/client/test_adapter.py
Configures platform clients with explicit retry statuses and header handling, and verifies the resulting RetryPolicy.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant _should_retry
  participant _retry_after
  participant Sleep
  Client->>_should_retry: evaluate HTTP response and RetryPolicy
  _should_retry->>_retry_after: parse retry-after headers when enabled
  _retry_after-->>_should_retry: retry delay or None
  _should_retry->>Sleep: wait using header delay or exponential backoff
  Sleep-->>Client: retry request
Loading

Suggested reviewers: a2bondar, aahunt-nv, ajaythorve

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preserving SDK retry behavior in NemoClient adapters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aircore-876-nemoclient-retry-fix/md

Comment @coderabbitai help to get the list of available commands.

@maxdubrinsky
maxdubrinsky marked this pull request as draft July 30, 2026 17:50
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 28323/36158 78.3% 62.7%
Integration Tests 16891/34876 48.4% 21.1%

Ahead of the AIRCORE-876 Models typed-client migration, split out as an
independent client-layer change touching nothing else.

The generic adapter now preserves the SDK's retry policy: Stainless
retry-decision and Retry-After headers are honored, with a fallback for
unreasonable Retry-After values, while the standalone policy keeps its
narrower retryable status set. No consumer behavior changes; covered by
expanded adapter and client-option tests.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@maxdubrinsky
maxdubrinsky force-pushed the aircore-876-nemoclient-retry-fix/md branch from 6f82df7 to 8119bb4 Compare July 30, 2026 18:05
@maxdubrinsky
maxdubrinsky marked this pull request as ready for review July 30, 2026 18:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/nemo_platform_plugin/tests/client/test_client_options.py (1)

743-747: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Async path never asserts the computed delay.

Sync tests verify retry-after parsing end-to-end; async only mocks asyncio.sleep. Add one async case asserting sleep.assert_awaited_once_with(...) so an async-specific regression in delay handling is caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nemo_platform_plugin/tests/client/test_client_options.py` around
lines 743 - 747, Extend the async retry test around
client.send(GET_ITEM(name="alice")) to retain the patched asyncio.sleep
AsyncMock and assert it was awaited exactly once with the computed retry delay.
Preserve the existing response and request-count assertions, and use the async
mock’s await-specific assertion.
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py (1)

159-182: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Backoff is uncapped and jitter-free.

backoff_base * 2**attempt grows without bound; adapter.py passes platform.max_retries, so a large value yields multi-minute blocking sleeps on the calling thread and synchronized retry storms across clients. The Stainless SDK caps delay (~8s) and adds jitter. Consider a max_backoff field plus jitter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py`
around lines 159 - 182, Update the retry delay calculation in the retry backoff
logic to cap exponential growth with a configurable max_backoff value and add
randomized jitter before returning the delay. Apply the bounded, jittered delay
consistently for exception, retryable-status, and retry-after paths while
preserving retry decisions and honoring server-provided retry timing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py`:
- Around line 134-141: Update _retry_after to catch OverflowError and OSError
alongside TypeError and ValueError when parsing retry_after and converting the
resulting datetime to a timestamp. Return None for these out-of-range HTTP-date
cases so the retry loop continues without propagating the exception.

---

Nitpick comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py`:
- Around line 159-182: Update the retry delay calculation in the retry backoff
logic to cap exponential growth with a configurable max_backoff value and add
randomized jitter before returning the delay. Apply the bounded, jittered delay
consistently for exception, retryable-status, and retry-after paths while
preserving retry decisions and honoring server-provided retry timing.

In `@packages/nemo_platform_plugin/tests/client/test_client_options.py`:
- Around line 743-747: Extend the async retry test around
client.send(GET_ITEM(name="alice")) to retain the patched asyncio.sleep
AsyncMock and assert it was awaited exactly once with the computed retry delay.
Preserve the existing response and request-count assertions, and use the async
mock’s await-specific assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8c56af85-3e5a-4082-bc85-c30cecf04165

📥 Commits

Reviewing files that changed from the base of the PR and between 6f82df7 and 8119bb4.

📒 Files selected for processing (5)
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/adapter.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/types.py
  • packages/nemo_platform_plugin/tests/client/test_adapter.py
  • packages/nemo_platform_plugin/tests/client/test_client_options.py

Comment on lines +134 to +141
try:
retry_date = email.utils.parsedate_to_datetime(retry_after)
except (TypeError, ValueError):
return None
if retry_date.tzinfo is None:
retry_date = retry_date.replace(tzinfo=timezone.utc)
delay = retry_date.timestamp() - time.time()
return delay if 0 < delay <= 60 else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against OverflowError/OSError from far-future HTTP-dates.

parsedate_to_datetime and datetime.timestamp() can raise OverflowError (or OSError on some platforms) for out-of-range dates, escaping _retry_after and aborting the retry loop with an unrelated exception.

🛡️ Proposed fix
             try:
                 retry_date = email.utils.parsedate_to_datetime(retry_after)
-            except (TypeError, ValueError):
+            except (TypeError, ValueError, OverflowError):
                 return None
             if retry_date.tzinfo is None:
                 retry_date = retry_date.replace(tzinfo=timezone.utc)
-            delay = retry_date.timestamp() - time.time()
+            try:
+                delay = retry_date.timestamp() - time.time()
+            except (OverflowError, OSError, ValueError):
+                return None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
retry_date = email.utils.parsedate_to_datetime(retry_after)
except (TypeError, ValueError):
return None
if retry_date.tzinfo is None:
retry_date = retry_date.replace(tzinfo=timezone.utc)
delay = retry_date.timestamp() - time.time()
return delay if 0 < delay <= 60 else None
try:
retry_date = email.utils.parsedate_to_datetime(retry_after)
except (TypeError, ValueError, OverflowError):
return None
if retry_date.tzinfo is None:
retry_date = retry_date.replace(tzinfo=timezone.utc)
try:
delay = retry_date.timestamp() - time.time()
except (OverflowError, OSError, ValueError):
return None
return delay if 0 < delay <= 60 else None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py`
around lines 134 - 141, Update _retry_after to catch OverflowError and OSError
alongside TypeError and ValueError when parsing retry_after and converting the
resulting datetime to a timestamp. Return None for these out-of-range HTTP-date
cases so the retry loop continues without propagating the exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant