Skip to content

UN-3344 [FIX] Unified retry for LLM and embedding providers#1886

Merged
muhammad-ali-e merged 12 commits intomainfrom
fix/unified-retry-llm-embedding
Apr 17, 2026
Merged

UN-3344 [FIX] Unified retry for LLM and embedding providers#1886
muhammad-ali-e merged 12 commits intomainfrom
fix/unified-retry-llm-embedding

Conversation

@chandrasekharan-zipstack
Copy link
Copy Markdown
Contributor

@chandrasekharan-zipstack chandrasekharan-zipstack commented Mar 31, 2026

What

  • Unified retry with exponential backoff for all LLM and embedding provider calls
  • Exposed max_retries in UI for embedding adapters (OpenAI, Azure, VertexAI, Ollama) and Ollama LLM — previously missing from the adapter form
  • DRY consolidation of retry decision logic into a single _get_retry_delay() helper used by all 4 retry paths

Why

  • max_retries configured in the adapter UI was silently ignored for:
    • LLM (httpx-based providers): Anthropic, Vertex AI, Bedrock, Mistral — litellm's retry only works for SDK-based providers (OpenAI/Azure)
    • All embedding providers: litellm has no wrapper-level retry for embeddings at all (embedding_with_retries() does not exist)
  • 4 out of 5 embedding adapters did not expose max_retries in the UI, so users could not configure retries even if the backend supported them
  • Retry decision logic was duplicated across call_with_retry, acall_with_retry, iter_with_retry, and retry_with_exponential_backoff

How

  • Self-managed retry at the SDK layer wrapping litellm.completion() / litellm.embedding() calls
  • Disable litellm's own retry to prevent double-retry: max_retries=0 (SDK-level) + num_retries=0 (litellm wrapper)
  • is_retryable_litellm_error() predicate uses duck-typing (no litellm import) to detect transient errors: status codes 408/429/500/502/503/504, ConnectionError, TimeoutError, and litellm/httpx exception class names
  • Stream retry (iter_with_retry) only retries before the first chunk is yielded — mid-stream failures propagate immediately
  • Extracted _get_retry_delay() shared helper — all retry functions delegate retry decisions to this single function

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

No breaking changes:

  • Existing adapters without max_retries in saved metadata: Pydantic defaults to None, code converts to 0 — no retry, identical to current behavior
  • New/edited adapters: UI form pre-fills max_retries=3, users can adjust
  • JSON schema additions are additive — no existing fields removed or changed (except Bedrock embedding default: 5 to 3 for consistency)
  • litellm caching, callbacks, and usage tracking still work per attempt since each retry goes through litellm.completion()/litellm.embedding()

Database Migrations

  • None

Env Config

  • No new env vars. Existing PLATFORM_SERVICE_MAX_RETRIES, PROMPT_SERVICE_MAX_RETRIES etc. are unaffected.

Relevant Docs

Related Issues or PRs

Dependencies Versions

  • No dependency changes

Notes on Testing

Tested locally with docker containers against:

Provider Error Type Retried? Backoff?
OpenAI LLM (SDK-based) Auth 401 No (correct) N/A
OpenAI LLM (SDK-based) Timeout Yes, 2 retries 1.2s, 2.5s
Anthropic LLM (httpx-based) Auth 401 No (correct) N/A
Anthropic LLM (httpx-based) Timeout Yes, 2 retries 1.2s, 2.2s
OpenAI Embedding Auth 401 No (correct) N/A
OpenAI Embedding Timeout Yes, 2 retries 1.0s, 2.3s

Screenshots

N/A — backend-only changes + JSON schema additions

Checklist

I have read and understood the Contribution Guidelines.

litellm's retry only works for SDK-based providers (OpenAI/Azure).
httpx-based providers (Anthropic, Vertex, Bedrock, Mistral) and ALL
embedding calls silently ignore max_retries. This adds self-managed
retry with exponential backoff at the SDK layer, disabling litellm's
own retry entirely for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds configurable retry support: adapter JSON schemas gain optional max_retries (mostly default 3; bedrock from 5→3). Embedding and LLM paths route LiteLLM calls through new shared retry helpers while disabling LiteLLM’s internal retry.

Changes

Cohort / File(s) Summary
Embedding Adapter Schemas
unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/azure.json, unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/openai.json, unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/ollama.json, unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/vertexai.json
Added optional max_retries property (number; minimum: 0; multipleOf: 1; default: 3) to each schema.
Bedrock Embedding Schema
unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json
Updated max_retries default from 53 and rephrased description.
LLM Adapter Schema
unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json
Added optional max_retries property (number; minimum: 0; multipleOf: 1; default: 3).
Embedding Module
unstract/sdk1/src/unstract/sdk1/embedding.py
Wrapped LiteLLM embedding calls with retry helpers (call_with_retry/acall_with_retry), added private _pop_retry_params() to extract/suppress LiteLLM retry params, adjusted async error handling, and added module logging.
LLM Module
unstract/sdk1/src/unstract/sdk1/llm.py
Routed sync/async/streaming completion calls through call_with_retry, acall_with_retry, iter_with_retry; added _disable_litellm_retry() to extract max_retries and force-disable LiteLLM internal retries.
Retry Utilities
unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py
Introduced retry infra: RETRYABLE_STATUS_CODES, _RETRYABLE_ERROR_NAMES, is_retryable_litellm_error(), _get_retry_delay(), and helpers call_with_retry, acall_with_retry, iter_with_retry; refactored retry_with_exponential_backoff and adjusted requests exception aliasing.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant Module as Embedding/LLM Module
    participant RetryUtil as Retry Utils
    participant LiteLLM as LiteLLM
    participant Provider as External Provider

    Client->>Module: Call get_embedding / completion
    Module->>Module: Extract max_retries from kwargs
    Module->>Module: Disable LiteLLM internal retry (set max_retries=0, num_retries=0)
    Module->>RetryUtil: Invoke call_with_retry / acall_with_retry / iter_with_retry

    loop Until success or max_retries exhausted
        RetryUtil->>LiteLLM: Execute wrapped call
        LiteLLM->>Provider: Send request
        alt Provider success
            Provider-->>LiteLLM: Return response
            LiteLLM-->>RetryUtil: Return result
            RetryUtil-->>Client: Return result
        else Transient provider error
            Provider-->>LiteLLM: Error
            LiteLLM-->>RetryUtil: Raise exception
            RetryUtil->>RetryUtil: is_retryable_litellm_error()? → True
            RetryUtil->>RetryUtil: Compute backoff delay
            RetryUtil->>RetryUtil: Wait & retry
        else Non-retryable error
            Provider-->>LiteLLM: Error
            LiteLLM-->>RetryUtil: Raise exception
            RetryUtil-->>Client: Raise exception
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main objective: implementing unified retry mechanisms for LLM and embedding providers, which aligns with the comprehensive changes across adapter configs and retry utilities.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required template sections: What, Why, How, breaking changes assessment, database migrations, env config, related issues, dependencies, testing notes, and checklist confirmation.
Docstring Coverage ✅ Passed Docstring coverage is 80.77% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/unified-retry-llm-embedding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Move retry loops out of LLM/Embedding classes into generic
call_with_retry, acall_with_retry, and iter_with_retry functions
in retry_utils.py. Both classes now call these directly instead
of maintaining their own retry helper methods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR fixes silent retry failures for LLM (httpx-based providers) and all embedding adapters by introducing self-managed retry at the SDK layer, exposing max_retries in the UI for previously missing adapters, and consolidating all retry decision logic into a single _get_retry_delay helper. The three issues raised in prior review threads — ConnectionError shadowing, name-only MRO check, and Azure double-retry via num_retries — are all correctly resolved.

Confidence Score: 5/5

Safe to merge — no P0/P1 issues; all three concerns from the previous review thread are fully resolved.

The implementation correctly addresses every prior finding: ConnectionError shadowing is fixed (RequestsConnectionError alias), MRO-based retryable-name check replaces class-name-only check, and pop_litellm_retry_kwargs explicitly sets num_retries=0 to prevent Azure double-retry. New call_with_retry/acall_with_retry/iter_with_retry wrappers are correct — iterator is properly closed before retry, retry boundary (has_yielded guard) is enforced, and exception context is preserved for logger.exception(). JSON schema additions are additive and wire cleanly to existing pydantic base models.

No files require special attention.

Important Files Changed

Filename Overview
unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py Core of the PR: adds is_retryable_litellm_error (MRO-safe, builtins-aware), _get_retry_delay (shared decision + backoff helper), pop_litellm_retry_kwargs (zeros both max_retries and num_retries), and call_with_retry/acall_with_retry/iter_with_retry wrappers. All previously flagged issues resolved.
unstract/sdk1/src/unstract/sdk1/llm.py complete/acomplete/stream_complete all migrated to pop_litellm_retry_kwargs + call_with_retry/acall_with_retry/iter_with_retry; litellm double-retry properly disabled.
unstract/sdk1/src/unstract/sdk1/embedding.py All four embedding paths (sync/async, single/batch) migrated to pop_litellm_retry_kwargs + call_with_retry/acall_with_retry; kwargs.copy() pattern prevents mutation of self.kwargs across retries.
unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/azure.json Added max_retries field (default 3) to Azure embedding UI schema; consistent with OpenAI/Bedrock embedding schemas.
unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/vertexai.json Added max_retries field (default 3); consistent with the pre-existing VertexAI embedding model which uses BaseEmbeddingParameters default timeout.
unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json Added max_retries field (default 3) to Ollama LLM; OllamaLLMParameters inherits max_retries from BaseChatCompletionParameters so pydantic validation handles it correctly.

Sequence Diagram

sequenceDiagram
    participant C as Caller LLM/Embedding
    participant P as pop_litellm_retry_kwargs
    participant R as call_with_retry or iter_with_retry
    participant D as _get_retry_delay
    participant L as litellm completion or embedding

    C->>P: kwargs including user max_retries
    P-->>C: extracted max_retries, kwargs zeroed for litellm

    loop attempt 0 to max_retries
        C->>R: fn lambda, max_retries, is_retryable_litellm_error
        R->>L: litellm call with max_retries=0 and num_retries=0
        alt Success
            L-->>R: response
            R-->>C: result
        else Transient error 408 429 5xx or APIConnectionError
            L-->>R: Exception
            R->>D: error, attempt, max_retries, predicate
            D->>D: check Retry-After header then exponential backoff
            D-->>R: delay seconds or None if give up or non-retryable
            alt delay returned
                R->>R: sleep delay then retry
            else None returned
                R-->>C: raise original exception
            end
        end
    end
Loading

Reviews (11): Last reviewed commit: "Merge branch 'main' into fix/unified-ret..." | Re-trigger Greptile

Comment thread unstract/sdk1/src/unstract/sdk1/embedding.py Outdated
Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py Outdated
Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py
chandrasekharan-zipstack and others added 2 commits April 1, 2026 16:07
- Extract _get_retry_delay() shared helper to eliminate duplicated
  retry decision logic across call_with_retry, acall_with_retry,
  iter_with_retry, and retry_with_exponential_backoff
- Add num_retries=0 to embedding._pop_retry_params() to fully
  disable litellm's internal retry for embedding calls
- Expose max_retries in UI JSON schemas for embedding adapters
  (OpenAI, Azure, VertexAI, Ollama) and Ollama LLM — previously
  the field existed in Pydantic models but wasn't shown to users,
  silently defaulting to 0 retries
- Add debug logging to LLM and Embedding retry parameter extraction
- Clarify docstrings distinguishing is_retryable_litellm_error()
  from is_retryable_error() (different exception hierarchies)
- Remove stale noqa: C901 from simplified retry_with_exponential_backoff

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dapters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…check

- Fix `requests.ConnectionError` shadowing Python's builtin `ConnectionError`
  in `is_retryable_litellm_error()` — rename import to `RequestsConnectionError`
  and use `builtins.ConnectionError` / `builtins.TimeoutError` explicitly
- Use `__mro__`-based class name check instead of `type(error).__name__`
  to also catch subclasses of retryable error types
- P1 (num_retries not zeroed) was already fixed in prior commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py (1)

254-317: Extract the sync retry loop instead of maintaining two copies.

retry_with_exponential_backoff() now reimplements the same attempt/sleep/decision flow as call_with_retry(), which is the SonarCloud failure on Line 254. Pulling the common loop into one helper will lower the branching here and keep the decorator path from drifting away from the SDK path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 254 - 317,
The retry logic in retry_with_exponential_backoff duplicates the
attempt/sleep/decision flow from call_with_retry; extract that shared loop into
a single helper function (e.g., _run_retry_loop or reuse call_with_retry
signature) that encapsulates attempts, calling the target, invoking
_get_retry_delay, sleeping, logging, and raising, then have
retry_with_exponential_backoff's decorator/wrapper call this helper (passing
func, max_retries, base_delay, multiplier, jitter, exceptions, logger_instance,
prefix, retry_predicate) to eliminate the duplicated flow and keep both the
decorator path and call_with_retry using the same implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py`:
- Around line 113-186: The three retry helpers (call_with_retry,
acall_with_retry, iter_with_retry) must validate max_retries is >= 0 before
entering their loops; add an early check in each function (call_with_retry,
acall_with_retry, iter_with_retry) that raises a ValueError (or similar) when
max_retries is negative so we fail fast instead of returning None or yielding
nothing when range(max_retries + 1) is empty; this mirrors the existing
decorator validation and prevents silent no-op behavior.
- Around line 33-57: The isinstance check in is_retryable_litellm_error
currently uses a shadowed ConnectionError (imported from requests.exceptions)
which narrows detection to requests-only errors; update the import/usage so the
check uses the built-in ConnectionError and TimeoutError types (i.e., remove or
rename the requests import and reference the built-ins in
is_retryable_litellm_error) and also add "APITimeoutError" to the
_RETRYABLE_ERROR_NAMES allowlist so OpenAI/OpenAI-mapped 408 timeout exceptions
are recognized by the name-based check.

---

Nitpick comments:
In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py`:
- Around line 254-317: The retry logic in retry_with_exponential_backoff
duplicates the attempt/sleep/decision flow from call_with_retry; extract that
shared loop into a single helper function (e.g., _run_retry_loop or reuse
call_with_retry signature) that encapsulates attempts, calling the target,
invoking _get_retry_delay, sleeping, logging, and raising, then have
retry_with_exponential_backoff's decorator/wrapper call this helper (passing
func, max_retries, base_delay, multiplier, jitter, exceptions, logger_instance,
prefix, retry_predicate) to eliminate the duplicated flow and keep both the
decorator path and call_with_retry using the same implementation.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b73e0a58-1ac6-4696-9a91-27485df74d1b

📥 Commits

Reviewing files that changed from the base of the PR and between 3b1a343 and f83404a.

📒 Files selected for processing (9)
  • unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/azure.json
  • unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json
  • unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/ollama.json
  • unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/openai.json
  • unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/vertexai.json
  • unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json
  • unstract/sdk1/src/unstract/sdk1/embedding.py
  • unstract/sdk1/src/unstract/sdk1/llm.py
  • unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py

Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py
Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py (2)

257-324: Consider extracting the inner wrapper function to reduce cognitive complexity.

SonarCloud flags cognitive complexity of 20 (allowed: 15). The nested decorator structure with try/except and conditional logging contributes to this. You could extract the "giving up" logging block or the entire wrapper body into a helper function.

Possible refactor approach
+def _log_final_failure(
+    logger_instance: logging.Logger,
+    func_name: str,
+    attempt: int,
+    prefix: str,
+) -> None:
+    """Log when retry attempts are exhausted."""
+    if attempt > 0:
+        logger_instance.exception(
+            "Giving up '%s' after %d attempt(s) for %s",
+            func_name,
+            attempt + 1,
+            prefix,
+        )
+
 def retry_with_exponential_backoff(...) -> Callable:
     ...
     def decorator(func: Callable) -> Callable:
         `@wraps`(func)
         def wrapper(*args: Any, **kwargs: Any) -> Any:
             for attempt in range(max_retries + 1):
                 try:
                     result = func(*args, **kwargs)
                     ...
                     return result
                 except exceptions as e:
                     delay = _get_retry_delay(...)
                     if delay is None:
-                        if attempt > 0:
-                            logger_instance.exception(
-                                "Giving up '%s' after %d attempt(s) for %s",
-                                func.__name__,
-                                attempt + 1,
-                                prefix,
-                            )
+                        _log_final_failure(logger_instance, func.__name__, attempt, prefix)
                         raise
                     time.sleep(delay)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 257 - 324,
The wrapper inside retry_with_exponential_backoff is too complex; extract the
retry loop or the "giving up" logging and sleep/raise logic into a helper
function to reduce cognitive complexity. Create a private helper (e.g.,
_execute_with_retries or _handle_attempt) that accepts func, args, kwargs,
max_retries, base_delay, multiplier, jitter, exceptions, retry_predicate,
prefix, and logger_instance and returns the result or raises; have wrapper call
this helper and keep wrapper only responsible for `@wraps` and invoking the
helper; ensure the helper uses _get_retry_delay and preserves the existing
logging messages (including the success log in wrapper when attempt > 0 or move
that logging into the helper) so behavior is unchanged.

138-145: Consider tightening type hints for fn parameter.

The current Callable[[], object] is imprecise:

  • acall_with_retry awaits the result, so fn should return an awaitable
  • iter_with_retry iterates the result, so fn should return an iterable

This won't cause runtime issues but weakens static type checking.

Suggested type hints
from collections.abc import Awaitable, Iterable
from typing import TypeVar

T = TypeVar("T")

async def acall_with_retry(
    fn: Callable[[], Awaitable[T]],
    *,
    max_retries: int,
    ...
) -> T:

def iter_with_retry(
    fn: Callable[[], Iterable[T]],
    *,
    max_retries: int,
    ...
) -> Generator[T, None, None]:

Also applies to: 160-167

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 138 - 145,
Tighten the type hints for acall_with_retry and iter_with_retry: introduce a
TypeVar T and change the fn parameter for acall_with_retry to Callable[[],
Awaitable[T]] with the function returning T, and change iter_with_retry to
accept Callable[[], Iterable[T]] and return Generator[T, None, None]; import
Awaitable and Iterable from collections.abc (and Generator if not already
present) and TypeVar from typing to make the signatures precise and improve
static checking while keeping existing runtime behavior (refer to function names
acall_with_retry and iter_with_retry and update their return annotations
accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py`:
- Around line 257-324: The wrapper inside retry_with_exponential_backoff is too
complex; extract the retry loop or the "giving up" logging and sleep/raise logic
into a helper function to reduce cognitive complexity. Create a private helper
(e.g., _execute_with_retries or _handle_attempt) that accepts func, args,
kwargs, max_retries, base_delay, multiplier, jitter, exceptions,
retry_predicate, prefix, and logger_instance and returns the result or raises;
have wrapper call this helper and keep wrapper only responsible for `@wraps` and
invoking the helper; ensure the helper uses _get_retry_delay and preserves the
existing logging messages (including the success log in wrapper when attempt > 0
or move that logging into the helper) so behavior is unchanged.
- Around line 138-145: Tighten the type hints for acall_with_retry and
iter_with_retry: introduce a TypeVar T and change the fn parameter for
acall_with_retry to Callable[[], Awaitable[T]] with the function returning T,
and change iter_with_retry to accept Callable[[], Iterable[T]] and return
Generator[T, None, None]; import Awaitable and Iterable from collections.abc
(and Generator if not already present) and TypeVar from typing to make the
signatures precise and improve static checking while keeping existing runtime
behavior (refer to function names acall_with_retry and iter_with_retry and
update their return annotations accordingly).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04b772de-c8c2-491a-9b94-28b8bcc53a9a

📥 Commits

Reviewing files that changed from the base of the PR and between f83404a and 43fed18.

📒 Files selected for processing (1)
  • unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py

…tries

- Add APITimeoutError to _RETRYABLE_ERROR_NAMES for explicit OpenAI
  SDK timeout coverage
- Add _validate_max_retries() guard to call_with_retry, acall_with_retry,
  iter_with_retry to fail fast on negative values instead of silently
  returning None

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py (3)

266-333: Consider reducing cognitive complexity.

SonarCloud flagged this function with cognitive complexity of 20 (allowed 15). The nested conditionals within the exception handlers contribute to this. Consider extracting the "giving up" logging block (lines 319-325) into a helper or simplifying the control flow.

💡 Suggested refactor
+def _log_giving_up(
+    logger_instance: logging.Logger,
+    func_name: str,
+    attempt: int,
+    prefix: str,
+) -> None:
+    """Log when giving up after all retry attempts."""
+    if attempt > 0:
+        logger_instance.exception(
+            "Giving up '%s' after %d attempt(s) for %s",
+            func_name,
+            attempt + 1,
+            prefix,
+        )
+

 def retry_with_exponential_backoff(
     ...
 ) -> Callable:
     def decorator(func: Callable) -> Callable:
         `@wraps`(func)
         def wrapper(*args: Any, **kwargs: Any) -> Any:
             for attempt in range(max_retries + 1):
                 try:
                     result = func(*args, **kwargs)
                     if attempt > 0:
                         logger_instance.info(
                             "Successfully completed '%s' after %d retry attempt(s)",
                             func.__name__,
                             attempt,
                         )
                     return result
                 except exceptions as e:
                     delay = _get_retry_delay(...)
                     if delay is None:
-                        if attempt > 0:
-                            logger_instance.exception(
-                                "Giving up '%s' after %d attempt(s) for %s",
-                                func.__name__,
-                                attempt + 1,
-                                prefix,
-                            )
+                        _log_giving_up(logger_instance, func.__name__, attempt, prefix)
                         raise
                     time.sleep(delay)
                 except Exception:
                     raise
         return wrapper
     return decorator
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 266 - 333,
The retry_with_exponential_backoff wrapper has high cognitive complexity due to
nested exception handling and an inline "giving up" logging block; extract that
block into a small helper (e.g., _log_giving_up or _handle_give_up) and call it
from inside the except exceptions as e: branch when _get_retry_delay returns
None, passing logger_instance, prefix, func.__name__, and attempt so the main
wrapper focuses on control flow only, reducing nesting; keep the existing calls
to _get_retry_delay and time.sleep unchanged and ensure the helper performs the
logger.exception call and any formatting before re-raising the exception.

145-152: Type hint should reflect awaitable return type.

The fn parameter is awaited on line 158, so the type hint should be Callable[[], Awaitable[object]] rather than Callable[[], object].

💡 Suggested fix
+from collections.abc import Awaitable
+
 async def acall_with_retry(
-    fn: Callable[[], object],
+    fn: Callable[[], Awaitable[object]],
     *,
     max_retries: int,
     retry_predicate: Callable[[Exception], bool],
     description: str = "",
     logger_instance: logging.Logger | None = None,
 ) -> object:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 145 - 152,
Update the type hint for the `fn` parameter in `acall_with_retry` to reflect
that it's awaited: change `Callable[[], object]` to `Callable[[],
Awaitable[object]]`, and add the necessary `Awaitable` import from `typing` (or
`collections.abc`) at the top of the module so the annotation is valid; keep the
function's async signature and return annotation as-is.

168-175: Type hint should reflect iterable return type.

The fn result is iterated on line 186, so the type hint should reflect this, e.g., Callable[[], Iterable[T]].

💡 Suggested fix
+from collections.abc import Iterable
+from typing import TypeVar
+
+T = TypeVar("T")
+
 def iter_with_retry(
-    fn: Callable[[], object],
+    fn: Callable[[], Iterable[T]],
     *,
     max_retries: int,
     retry_predicate: Callable[[Exception], bool],
     description: str = "",
     logger_instance: logging.Logger | None = None,
-) -> Generator:
+) -> Generator[T, None, None]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 168 - 175,
The type hint for iter_with_retry is wrong because fn is expected to return an
iterable; update the signature to use a generic TypeVar (e.g., T) and change
fn's type to Callable[[], Iterable[T]] and the function's return type to
Generator[T, None, None] so callers of iter_with_retry and the loop over fn()
have correct typing; ensure you import or declare typing.TypeVar and Iterable
where needed and update any related annotations in iter_with_retry accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py`:
- Around line 266-333: The retry_with_exponential_backoff wrapper has high
cognitive complexity due to nested exception handling and an inline "giving up"
logging block; extract that block into a small helper (e.g., _log_giving_up or
_handle_give_up) and call it from inside the except exceptions as e: branch when
_get_retry_delay returns None, passing logger_instance, prefix, func.__name__,
and attempt so the main wrapper focuses on control flow only, reducing nesting;
keep the existing calls to _get_retry_delay and time.sleep unchanged and ensure
the helper performs the logger.exception call and any formatting before
re-raising the exception.
- Around line 145-152: Update the type hint for the `fn` parameter in
`acall_with_retry` to reflect that it's awaited: change `Callable[[], object]`
to `Callable[[], Awaitable[object]]`, and add the necessary `Awaitable` import
from `typing` (or `collections.abc`) at the top of the module so the annotation
is valid; keep the function's async signature and return annotation as-is.
- Around line 168-175: The type hint for iter_with_retry is wrong because fn is
expected to return an iterable; update the signature to use a generic TypeVar
(e.g., T) and change fn's type to Callable[[], Iterable[T]] and the function's
return type to Generator[T, None, None] so callers of iter_with_retry and the
loop over fn() have correct typing; ensure you import or declare typing.TypeVar
and Iterable where needed and update any related annotations in iter_with_retry
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22f7cf9d-7244-483f-90ab-c81ca438acc8

📥 Commits

Reviewing files that changed from the base of the PR and between 43fed18 and c8054e8.

📒 Files selected for processing (1)
  • unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py

Copy link
Copy Markdown
Contributor

@pk-zipstack pk-zipstack left a comment

Choose a reason for hiding this comment

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

@chandrasekharan-zipstack Looks like there are some comments from sonarcloud. Please check them out. LGTM otherwise.

@chandrasekharan-zipstack chandrasekharan-zipstack changed the title [FIX] Unified retry for LLM and embedding providers UN-3344 [FIX] Unified retry for LLM and embedding providers Apr 17, 2026
…lause

Address SonarCloud findings on PR #1886:
- S3776: Flatten retry_with_exponential_backoff.wrapper by moving the
  success logging + return out of the try block and using `continue` in
  the retry path, so the except branch only handles the give-up case.
- S2737: Drop the `except Exception: raise` clause — it was a no-op that
  added complexity without changing behavior (non-matching exceptions
  propagate naturally).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py (2)

292-328: Optional: trim one point of cognitive complexity to satisfy SonarCloud (16 → 15).

SonarCloud reports 16 vs. threshold 15. The easiest reduction is extracting the give-up logging into a small helper so wrapper has one less nested branch; alternatively, just raise the threshold if the team prefers. Not blocking.

♻️ Example extraction
+    def _log_giveup(func: Callable, attempt: int) -> None:
+        if attempt > 0:
+            logger_instance.exception(
+                "Giving up '%s' after %d attempt(s) for %s",
+                func.__name__,
+                attempt + 1,
+                prefix,
+            )
+
     def decorator(func: Callable) -> Callable:
         `@wraps`(func)
         def wrapper(*args: Any, **kwargs: Any) -> Any:  # noqa: ANN401
             for attempt in range(max_retries + 1):
                 try:
                     result = func(*args, **kwargs)
                 except exceptions as e:
                     delay = _get_retry_delay(
                         e, attempt, max_retries, retry_predicate, prefix,
                         logger_instance, base_delay, multiplier, 60.0, jitter,
                     )
                     if delay is not None:
                         time.sleep(delay)
                         continue
-                    if attempt > 0:
-                        logger_instance.exception(
-                            "Giving up '%s' after %d attempt(s) for %s",
-                            func.__name__,
-                            attempt + 1,
-                            prefix,
-                        )
+                    _log_giveup(func, attempt)
                     raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 292 - 328,
The wrapper function inside decorator has one extra nested branch for the "give
up" logging; extract that logic into a small helper function (e.g., _log_give_up
or _log_and_raise) referenced from decorator/wrapper so wrapper no longer
contains the nested if that calls logger_instance.exception and raise; call the
helper with parameters (logger_instance, func.__name__, attempt, prefix) after
_get_retry_delay returns None and before re-raising the exception, and keep
existing behavior of logging only when attempt > 0 and then re-raising the
exception.

145-175: Tighten fn type hints for async and generator variants.

acall_with_retry awaits fn() and iter_with_retry iterates over fn(), but both declare fn: Callable[[], object]. Type checkers will not catch a caller that passes a plain sync function into acall_with_retry, or a non-iterable factory into iter_with_retry. Recommend narrowing:

♻️ Suggested tightening
-from collections.abc import Callable, Generator
+from collections.abc import Awaitable, Callable, Generator, Iterable
@@
 async def acall_with_retry(
-    fn: Callable[[], object],
+    fn: Callable[[], Awaitable[object]],
     *,
@@
 def iter_with_retry(
-    fn: Callable[[], object],
+    fn: Callable[[], Iterable[object]],
     *,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 145 - 175,
The signatures for acall_with_retry and iter_with_retry should be made generic
and their fn arguments narrowed: introduce a TypeVar T and change
acall_with_retry to async def acall_with_retry(fn: Callable[[], Awaitable[T]],
..., ) -> T so the checker knows fn is awaitable, and change iter_with_retry to
def iter_with_retry(fn: Callable[[], Iterable[T]], ..., ) -> Generator[T, None,
None] (or Callable[[], Iterator[T]] if fn yields an iterator) so the checker
knows fn returns an iterable; update imports (TypeVar, Awaitable,
Iterable/Iterator) and adjust the return type annotations accordingly while
keeping the existing behavior in the bodies of acall_with_retry and
iter_with_retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py`:
- Around line 292-328: The wrapper function inside decorator has one extra
nested branch for the "give up" logging; extract that logic into a small helper
function (e.g., _log_give_up or _log_and_raise) referenced from
decorator/wrapper so wrapper no longer contains the nested if that calls
logger_instance.exception and raise; call the helper with parameters
(logger_instance, func.__name__, attempt, prefix) after _get_retry_delay returns
None and before re-raising the exception, and keep existing behavior of logging
only when attempt > 0 and then re-raising the exception.
- Around line 145-175: The signatures for acall_with_retry and iter_with_retry
should be made generic and their fn arguments narrowed: introduce a TypeVar T
and change acall_with_retry to async def acall_with_retry(fn: Callable[[],
Awaitable[T]], ..., ) -> T so the checker knows fn is awaitable, and change
iter_with_retry to def iter_with_retry(fn: Callable[[], Iterable[T]], ..., ) ->
Generator[T, None, None] (or Callable[[], Iterator[T]] if fn yields an iterator)
so the checker knows fn returns an iterable; update imports (TypeVar, Awaitable,
Iterable/Iterator) and adjust the return type annotations accordingly while
keeping the existing behavior in the bodies of acall_with_retry and
iter_with_retry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9daf0733-561a-496c-a360-99875598852e

📥 Commits

Reviewing files that changed from the base of the PR and between c8054e8 and f186fb0.

📒 Files selected for processing (1)
  • unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py

…e complexity

Sonar still flagged retry_with_exponential_backoff at complexity 16 after
the previous flatten. Nested def decorator / def wrapper counted against
the outer function's score. Move the retry body to a module-level
_invoke_with_retries helper so the decorator factory just delegates,
bringing the outer function well under the 15 threshold.

Behavior is unchanged — all paths (success, retry, give-up, non-retryable
propagate) are preserved and covered by the existing SDK1 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py (3)

87-97: Minor: debug log counter vs. warning counter use different denominators.

The debug line logs attempt+1/max_retries+1 (total attempts including initial try), while the warning on Line 101-103 logs attempt+1/max_retries (retries only). When reading logs for the same failure, these will show different /N totals and be confusing. Additionally, on the last attempt (attempt == max_retries) the debug line still prints retryable=True even though the function returns None and the caller re-raises — the log reader sees “retryable=True” for a call that was not retried.

Consider either unifying the denominators or appending a "giving up" branch before returning None:

Suggested tweak
-    if not should_retry or attempt >= max_retries:
-        return None
+    if not should_retry or attempt >= max_retries:
+        logger_instance.debug(
+            "Giving up: attempts exhausted or non-retryable (attempt=%d, max_retries=%d, description=%s)",
+            attempt + 1, max_retries, description,
+        )
+        return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 87 - 97,
The debug and warning logs in the retry decision flow are inconsistent and can
mislead: update the logging in the function that calls logger_instance.debug
(use symbols attempt, max_retries, should_retry, description) so both messages
use the same denominator (either total attempts = max_retries+1 or retries =
max_retries), and add a conditional "giving up" log path just before returning
None when not should_retry or attempt >= max_retries so the message shows
retryable=False (or explicitly states giving up) for the final non-retry case;
ensure you adjust the debug message's retryable value to reflect the actual
branch taken.

176-198: Docstring: clarify that fn must return a fresh iterable on each call.

Each retry re-invokes fn(), which assumes fn is a factory (e.g., lambda: litellm.completion(stream=True, ...)). Passing an already-materialized generator would be silently wrong: after the first failure the exhausted generator would either yield nothing or raise StopIteration/the same error. Worth a one-liner in the docstring so callers don't pass a bare iterator.

Suggested wording
-    """Yield from fn() with retry. Only retries before the first yield.
-
-    Once items have been yielded to the caller a mid-iteration failure is
-    raised immediately — partial output can't be un-yielded.
-    """
+    """Yield from fn() with retry. Only retries before the first yield.
+
+    `fn` must be a factory that returns a *fresh* iterable on each call
+    (e.g., ``lambda: litellm.completion(stream=True, ...)``). A bare
+    generator must not be passed, since retrying would re-consume an
+    already-exhausted iterator.
+
+    Once items have been yielded to the caller a mid-iteration failure is
+    raised immediately — partial output can't be un-yielded.
+    """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 176 - 198,
Update the function docstring to explicitly state that the parameter fn must be
a zero-argument factory that returns a fresh iterable each time it is called
(i.e., pass a callable like lambda: generator() rather than an
already-materialized iterator), because the code re-invokes fn() on each retry;
reference the use of fn() and the retry loop (for attempt in range(max_retries +
1)) and the has_yielded logic so callers understand why a fresh iterable is
required.

145-165: Type hints: fn parameters are incorrectly typed as Callable[[], object] but actual usage differs.

  • acall_with_retry awaits the return value (await fn()), so fn should be typed as Callable[[], Awaitable[object]]
  • iter_with_retry yields from the return value, so fn should be typed as Callable[[], Iterable[object]]

Current imports at line 10 only include Callable and Generator. Add Awaitable and Iterable to fix type checker compatibility.

Suggested fix
-from collections.abc import Callable, Generator
+from collections.abc import Awaitable, Callable, Generator, Iterable
@@
 async def acall_with_retry(
-    fn: Callable[[], object],
+    fn: Callable[[], Awaitable[object]],
     *,
@@
 def iter_with_retry(
-    fn: Callable[[], object],
+    fn: Callable[[], Iterable[object]],
     *,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py` around lines 145 - 165,
The type hints for the retry utilities are incorrect: update the imports to
include Awaitable and Iterable, then change acall_with_retry's fn annotation
from Callable[[], object] to Callable[[], Awaitable[object]] and change
iter_with_retry's fn annotation from Callable[[], object] to Callable[[],
Iterable[object]] so the declared callable return types match actual usage;
locate and update the import list (where Callable and Generator are imported)
and the fn parameter annotations in acall_with_retry and iter_with_retry
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py`:
- Around line 87-97: The debug and warning logs in the retry decision flow are
inconsistent and can mislead: update the logging in the function that calls
logger_instance.debug (use symbols attempt, max_retries, should_retry,
description) so both messages use the same denominator (either total attempts =
max_retries+1 or retries = max_retries), and add a conditional "giving up" log
path just before returning None when not should_retry or attempt >= max_retries
so the message shows retryable=False (or explicitly states giving up) for the
final non-retry case; ensure you adjust the debug message's retryable value to
reflect the actual branch taken.
- Around line 176-198: Update the function docstring to explicitly state that
the parameter fn must be a zero-argument factory that returns a fresh iterable
each time it is called (i.e., pass a callable like lambda: generator() rather
than an already-materialized iterator), because the code re-invokes fn() on each
retry; reference the use of fn() and the retry loop (for attempt in
range(max_retries + 1)) and the has_yielded logic so callers understand why a
fresh iterable is required.
- Around line 145-165: The type hints for the retry utilities are incorrect:
update the imports to include Awaitable and Iterable, then change
acall_with_retry's fn annotation from Callable[[], object] to Callable[[],
Awaitable[object]] and change iter_with_retry's fn annotation from Callable[[],
object] to Callable[[], Iterable[object]] so the declared callable return types
match actual usage; locate and update the import list (where Callable and
Generator are imported) and the fn parameter annotations in acall_with_retry and
iter_with_retry accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9c300ebb-e374-45df-a953-22e5ecc58150

📥 Commits

Reviewing files that changed from the base of the PR and between f186fb0 and d0c864f.

📒 Files selected for processing (1)
  • unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py

Copy link
Copy Markdown
Contributor

@muhammad-ali-e muhammad-ali-e left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown
Contributor

@muhammad-ali-e muhammad-ali-e left a comment

Choose a reason for hiding this comment

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

Line-by-line review. Not blocking — PR is fundamentally sound and CI is green. Main asks before merge:

  1. Add unit tests for the new call_with_retry / acall_with_retry / iter_with_retry / is_retryable_litellm_error / _get_retry_delay. The existing test_retry_utils.py (598 lines) has zero coverage for any of the new symbols — suggest at minimum: success path, exhaust-and-raise, non-retryable short-circuit, iter retry before first yield, iter no-retry after yield, is_retryable_litellm_error status_code + MRO-walk + builtin-vs-requests disambiguation.
  2. Close the generator on retry in iter_with_retry (see inline).
  3. Consider honoring Retry-After headers on 429/503 for OpenAI/Azure — disabling SDK-level retry loses this (see inline).

Rest are polish. See inline comments.

Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py
Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py
Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py
Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py Outdated
Comment thread unstract/sdk1/src/unstract/sdk1/utils/retry_utils.py
Comment thread unstract/sdk1/src/unstract/sdk1/embedding.py Outdated
Comment thread unstract/sdk1/src/unstract/sdk1/llm.py
Comment thread unstract/sdk1/src/unstract/sdk1/llm.py Outdated
…e-up log

Address review comments on PR #1886:

- #10 (resource leak): close the generator returned by fn() before
  retrying in iter_with_retry — otherwise streaming providers leak an
  in-flight HTTP socket until GC.
- #12 (behavioral regression): when we zero out SDK/wrapper retries we
  also lose the OpenAI SDK's native Retry-After handling on 429/503.
  _get_retry_delay now checks error.response.headers["retry-after"] and
  uses that value ahead of exponential backoff. HTTP-date form is not
  parsed; those fall back to backoff.
- #8 (observability gap): move the "Giving up ... after N attempt(s)"
  log into _get_retry_delay so all four retry helpers (call_with_retry,
  acall_with_retry, iter_with_retry, decorator) share the same
  exhaustion signal. Previously only the decorator path logged it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… wrappers

Address review comments on PR #1886:

- #9 (typing): call_with_retry / acall_with_retry / iter_with_retry
  previously returned `object`, erasing caller type info. Add PEP 695
  generics so the return type flows from the wrapped callable:
  acall_with_retry now takes Callable[[], Awaitable[T]] and
  iter_with_retry takes Callable[[], Iterable[T]] -> Generator[T, ...].
- #11 / #13 (DRY): `_pop_retry_params` in embedding.py and
  `_disable_litellm_retry` in llm.py were identical logic. Lift to
  shared `pop_litellm_retry_kwargs` helper in retry_utils.py and delete
  both methods.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 223 passed, 0 failed (223 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_timeout\_passed\_to\_client\_post}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_none\_timeout\_passed\_to\_client\_post}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_httpx\_timeout\_object\_forwarded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_cohere\_handler\_patched}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_bedrock\_handler\_patched}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_patch\_module\_loaded\_via\_embedding\_import}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_round\_trip\_serialization}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_json\_serializable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_enum\_values\_normalized}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_string\_values\_accepted}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_auto\_generates\_request\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_explicit\_request\_id\_preserved}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_optional\_organization\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_empty\_executor\_params\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_complex\_executor\_params}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_validation\_rejects\_empty\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_all\_operations\_accepted}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_from\_dict\_missing\_optional\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_round\_trip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_round\_trip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_json\_serializable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_requires\_error\_message}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_allows\_no\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_rejects\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_factory}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_factory\_no\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_error\_none\_in\_success\_dict}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_error\_in\_failure\_dict}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_default\_empty\_dicts}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_from\_dict\_missing\_optional\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_extract}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_index}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_answer\_prompt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_cannot\_instantiate\_abstract}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_concrete\_subclass\_works}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_execute\_returns\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_and\_get}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_returns\_fresh\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_as\_decorator}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_list\_executors}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_list\_executors\_empty}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_unknown\_raises\_key\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_unknown\_lists\_available}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_duplicate\_name\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_non\_subclass\_raises\_type\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_non\_class\_raises\_type\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_clear}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_execute\_through\_registry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_dispatches\_to\_correct\_executor}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_unknown\_executor\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_executor\_exception\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_exception\_result\_has\_elapsed\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_successful\_result\_passed\_through}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_executor\_returning\_failure\_is\_not\_wrapped}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_sends\_task\_and\_returns\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_uses\_default\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_timeout\_from\_env}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_explicit\_timeout\_overrides\_env}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_timeout\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_generic\_exception\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_async\_returns\_task\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_async\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_failure\_result\_from\_executor}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_context\_serialized\_correctly}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_sends\_link\_and\_link\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_success\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_error\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_callbacks}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_returns\_async\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_context\_serialized}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_custom\_task\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_task\_id\_omits\_kwarg}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_platform\_api\_key\_returned}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_platform\_api\_key\_missing\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_other\_env\_var\_from\_environ}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_missing\_env\_var\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_empty\_env\_var\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_log\_routes\_to\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_log\_respects\_level}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_error\_and\_exit\_raises\_sdk\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_error\_and\_exit\_wraps\_original}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_prefixes\_when\_missing}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_does\_not\_double\_prefix}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_blank\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_disabled\_by\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_excludes\_control\_fields\_from\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_enabled\_with\_budget}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_overrides\_user\_temperature}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_enabled\_without\_budget\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_budget\_tokens\_invalid\_type\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_budget\_tokens\_too\_small\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_preserves\_existing\_thinking\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_thinking\_controls\_not\_pydantic\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_api\_key\_is\_required}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_adapter\_identity}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_enable\_thinking\_default\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_budget\_tokens\_conditional}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_adapter\_registration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_id\_format}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_adapter\_type}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_name}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_provider}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_loads}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_no\_batch\_size\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_api\_key\_password\_format}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_model\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_adds\_prefix}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_idempotent}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_empty\_string\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_whitespace\_only\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_none\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_missing\_key\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_empty\_model\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_none\_model\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_missing\_api\_key\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_calls\_validate\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_embed\_batch\_size\_none\_by\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_embed\_batch\_size\_preserved}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_strips\_extra\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_includes\_base\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_reuses\_llm\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_returns\_llmcompat\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_sets\_model\_name}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_does\_not\_call\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_complete\_delegates\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_chat\_delegates\_to\_llm\_complete}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_chat\_forwards\_kwargs\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_complete\_forwards\_kwargs\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_acomplete\_delegates\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_achat\_delegates\_to\_llm\_acomplete}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_stream\_chat\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_stream\_complete\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_astream\_chat\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_astream\_complete\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_metadata\_returns\_emulated\_type}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_get\_model\_name\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_get\_metrics\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_test\_connection\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_message\_role\_values}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_chat\_message\_defaults}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_chat\_response\_message\_access}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_completion\_response\_text}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_llm\_metadata\_defaults}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_single\_user\_message}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_none\_content\_becomes\_empty\_string}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_preserves\_all\_messages}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_multi\_turn\_conversation}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_empty\_messages\_returns\_empty\_string}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_string\_role\_fallback}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{223}}$$ $$\textcolor{#23d18b}{\tt{223}}$$

@sonarqubecloud
Copy link
Copy Markdown

@muhammad-ali-e muhammad-ali-e merged commit e114dcf into main Apr 17, 2026
8 checks passed
@muhammad-ali-e muhammad-ali-e deleted the fix/unified-retry-llm-embedding branch April 17, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants