Skip to content

fix: extend provider error boundary to streaming and batch paths - #830

Merged
seonghobae merged 5 commits into
mainfrom
fix/stream-batch-provider-error-boundary
Aug 25, 2026
Merged

fix: extend provider error boundary to streaming and batch paths#830
seonghobae merged 5 commits into
mainfrom
fix/stream-batch-provider-error-boundary

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the remaining CWE-209 leak paths that survived #771: raw provider errors could still escape through (1) the SSE streaming generator, (2) the Batch API path, and (3) model discovery when a raw ConnectionResetError/OSError (not a URLError) occurred.

Changes

  • _stream_send: any mid-stream failure now raises one package-owned error (provider <id> streaming request failed) with cause severed. The terminal tool-stop SSE contract is preserved: tool-stop HTTP errors still convert to the package-owned stop error so callers keep 409 semantics.
  • batch_chat: upload/poll/download failures surface provider <id> batch request failed without raw urllib text.
  • discover_provider_models: catches OSError (covers ConnectionError) and maps it to stable transport_error.
  • ADR 0011 extended with decision item 6 covering streaming/batch; verification list updated.
  • Tests: raw-500 SSE provider test asserting no URL/body leakage and severed cause; batch boundary test; connection-reset discovery regression; mock-batch unchanged guard.

Supersedes the still-valid delta of #807 (closed as superseded by #771); this PR is rebased on current main.


Open in Devin Review

- _stream_send: mid-stream failures surface one package-owned error; the
  terminal tool-stop SSE contract is preserved (CWE-209)
- batch_chat: upload/poll/download failures no longer leak raw urllib text
- model discovery: raw connection resets (OSError, non-URLError) map to the
  stable transport_error code
- ADR 0011: document the streaming/batch boundary extension

Supersedes the still-valid delta of #807 after #771 landed the core boundary.
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 7 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a9ca43c-be33-4269-9569-c37ed57ce4ba

📥 Commits

Reviewing files that changed from the base of the PR and between 87c6c66 and faac2d4.

📒 Files selected for processing (6)
  • contextual_orchestrator/model_discovery.py
  • contextual_orchestrator/orchestrator.py
  • docs/planning/adrs/0011-provider-error-boundary.md
  • tests/test_model_discovery.py
  • tests/test_provider_reliability.py
  • tests/test_true_streaming.py

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.

devin-ai-integration[bot]

This comment was marked as resolved.

A provider usage-only frame can emit choices: [] or omit choices entirely.
The previous  raised IndexError when the key
existed but the list was empty, and the broad provider error boundary would
then abort an otherwise valid stream. Normalise the choices list before
indexing and add a regression test covering both empty and missing choices.

Devin Review: contextual-orchestrator#830

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae
seonghobae enabled auto-merge August 25, 2026 02:25

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Open in Devin Review

Comment on lines +1172 to +1173
choices = chunk.get("choices") or [{}]
delta = (choices[0] or {}).get("delta", {}).get("content")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 Info: Explicit null delta still aborts the stream

The new choices = chunk.get("choices") or [{}] fixes the empty-list IndexError, but a frame with "delta": null still makes None.get("content") raise, which the broad except turns into a stream abort. This matches the old one-liner's behavior, so it is not a regression.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +219 to +222
except (urllib.error.URLError, TimeoutError, ValueError, OSError) as exc:
# OSError covers ConnectionError/reset failures that are not URLError
# subclasses, so a raw provider transport failure can never escape the
# discovery boundary with provider text attached.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 Info: OSError catch subsumes URLError and TimeoutError

Adding OSError makes the existing URLError and TimeoutError tuple entries redundant (both subclass OSError). Code mapping is unaffected because _provider_discovery_error_code checks the more specific types first; JSONDecodeError is a ValueError, not OSError, so it still yields invalid_response.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Merge-gate evidence (2026-08-24): Deep diff review + fixes applied; all required checks green on current head except strix (org-wide NVIDIA NIM quota exhaustion — external provider-capacity blocker; serialization fix in ContextualWisdomLab/.github#1297). Full local suite green on this head.

# Conflicts:
#	contextual_orchestrator/orchestrator.py

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Open in Devin Review

Comment on lines +445 to +446
def _batch_run(self, agent, requests, temperature, poll_interval, poll_timeout, destination=None): # type: ignore[override]
raise RuntimeError("provider-secret-batch-body")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Batch boundary test never exercises the secret it claims to scrub

The RawBatchFailureClient._batch_run stub omits the effort_profile parameter that batch_chat passes positionally (_batch_run call), so the call raises TypeError before the stub body runs. The stub's RuntimeError("provider-secret-batch-body") never executes, so the assertion that this string is absent from the wrapped error is vacuous and the regression guard proves nothing.

Suggested change
def _batch_run(self, agent, requests, temperature, poll_interval, poll_timeout, destination=None): # type: ignore[override]
raise RuntimeError("provider-secret-batch-body")
def _batch_run(self, agent, requests, temperature, poll_interval, poll_timeout, destination=None, effort_profile=None): # type: ignore[override]
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1227 to +1234
except Exception as exc: # noqa: BLE001 - provider error boundary (CWE-209)
# The gateway's own terminal tool-stop contract must survive the
# boundary: convert the provider HTTP shape into the package-owned
# stop error so callers keep the 409 semantics they rely on.
if _is_tool_execution_stopped(exc):
raise _provider_tool_execution_stopped(agent) from None
raise
if isinstance(exc, ToolFallbackStoppedError):
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 Info: Tool-stop check safe on non-HTTP exceptions

_stream_send now runs _is_tool_execution_stopped(exc) on any exception. For non-HTTPError values the exc.read(...) call hits the AttributeError guard at _is_tool_execution_stopped and returns False, so they correctly fall through to the package-owned wrap. GeneratorExit is BaseException and is not caught.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Merge-gate evidence (2026-08-24): Deep review + integration complete; all required checks green on current head except strix (org-wide NVIDIA NIM quota exhaustion — external provider-capacity blocker; serialization fix in ContextualWisdomLab/.github#1297). Full local suite green.

@seonghobae
seonghobae merged commit 60c543d into main Aug 25, 2026
32 of 33 checks passed
@seonghobae
seonghobae deleted the fix/stream-batch-provider-error-boundary branch August 25, 2026 05:45
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.

1 participant