Skip to content

[integrations][openai] Let provider exceptions reach the caller unwrapped - #989

Open
weiqingy wants to merge 2 commits into
apache:mainfrom
weiqingy:936-unwrap-openai-exceptions
Open

[integrations][openai] Let provider exceptions reach the caller unwrapped#989
weiqingy wants to merge 2 commits into
apache:mainfrom
weiqingy:936-unwrap-openai-exceptions

Conversation

@weiqingy

@weiqingy weiqingy commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Linked issue: #936

Purpose of change

Case 3 of #936. The three OpenAI-family connections caught every exception from a chat call and rethrew it as a generic RuntimeException, while the Python connections let the SDK exception propagate unchanged. A caller writing against both languages saw different failure types for the same provider condition.

The SDK exceptions already carry the HTTP status and the provider's error payload, so the wrapper only forced callers to inspect a cause. This removes it from OpenAICompletionsConnection, AzureOpenAIChatModelConnection and OpenAIResponsesModelConnection, which aligns Java with Python. Local validation failures continue to raise IllegalArgumentException.

There is a second effect worth calling out. Each try also covered request building, so a local validation failure was rewrapped too. A tool message missing its externalId reached the caller as RuntimeException("Failed to call OpenAI chat completions API.") rather than as the IllegalArgumentException that OpenAIChatCompletionsUtils actually threw. Azure already had a catch (IllegalArgumentException e) { throw e; } guarding against exactly this, which is now unnecessary and is removed with the rest.

Out of scope: the wrappers around tool schema and tool argument JSON handling stay. Those wrap a checked JsonProcessingException from local work rather than a provider error, so they cannot propagate unwrapped.

Tests

OpenAICompletionsConnectionTest.testRequestBuildingFailurePropagatesUnwrapped and the same test in a new OpenAIResponsesModelConnectionTest call chat with a tool message that has no externalId and assert the IllegalArgumentException reaches the caller with its own type and message. Request building throws before the client is touched, so neither test needs a live API call.

I verified both tests discriminate rather than pass by construction: restoring the old catch block fails testRequestBuildingFailurePropagatesUnwrapped on the assertion, since the wrapper produces a plain RuntimeException that is not an IllegalArgumentException.

Azure gets no new test. Its IllegalArgumentException already propagated, so the only behavior that changed there is what happens to a provider SDK exception, and reaching that needs a live call through the final OpenAIClient.

mvn -pl integrations/chat-models/openai test passes, 72 tests. Spotless and RAT are clean.

API

No public API change. chat keeps its signature; only the exception type a caller observes on failure changes, from RuntimeException wrapping the SDK exception to the SDK exception itself.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

Was this patch authored or co-authored using generative AI tooling?

  • Yes
  • No

Generated-by: Claude Code 2.1.226

@github-actions github-actions Bot added doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue. labels Aug 9, 2026

@wenjin272 wenjin272 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.

The new tests only exercise a request-building IllegalArgumentException, not the provider/SDK exception propagation that is the main contract of this PR. They guard against restoring the exact broad catch blocks, but would still pass if a future change caught and wrapped only OpenAI SDK exceptions. The Azure path has no new regression coverage at all, and its IllegalArgumentException already propagated before this PR, so the current suite cannot distinguish its provider-error behavior. Could we use a local fake HTTP endpoint returning a 4xx for Chat Completions, Responses, and Azure, then assert the concrete SDK exception type plus its status/error payload? That would verify the behavior at the level where the issue occurs and cover all three changed paths.

@weiqingy

Copy link
Copy Markdown
Collaborator Author

@wenjin272 Thanks for review.

You were right, and it wasn't hypothetical. I wrapped only com.openai.errors.OpenAIException around the SDK call in OpenAICompletionsConnection, and the whole existing suite still passed.

Added FakeOpenAIErrorEndpoint, a small local HTTP server that returns a 400 with an OpenAI error body, and one testProviderErrorPropagatesUnwrapped in each of the three connection test classes. Each asserts BadRequestException and the code from the payload. No new dependency, nothing under src/main. 72 tests to 75. That same wrapper now fails exactly those three tests and nothing else, Azure included.

Two caveats:

  • Asserting statusCode() proves nothing on its own, since BadRequestException always returns 400. That line passes as soon as the type matches. The real check is the payload: change what the fake sends back and all three tests fail.
  • I only cover 400, not 4xx generally. A 429 or a 5xx gets retried, so it costs three requests instead of one. 400 keeps the test quick.

The two testRequestBuildingFailurePropagatesUnwrapped tests stay. Wrapping only buildRequest(...) fails those two and not the new ones, so they catch different bugs.

…pped

The three OpenAI-family connections caught every exception from a chat call
and rethrew it as a generic RuntimeException. The SDK exceptions already
carry the HTTP status and the provider's error payload, so wrapping them
forced callers to unwrap a cause to see what went wrong, and it diverged
from the Python connections, which propagate the SDK exception as-is.

The catch also covered request building, so a local validation failure such
as a tool message missing its externalId surfaced as "Failed to call OpenAI
chat completions API" rather than as the IllegalArgumentException it is.

Generated-by: Claude Code 2.1.226
…endpoint

The existing tests only exercised a request-building IllegalArgumentException,
so they would still pass if a future change caught and wrapped only OpenAI SDK
exceptions, and the Azure path had no coverage of the change at all.

FakeOpenAIErrorEndpoint serves an OpenAI error envelope over loopback, letting
each connection's error path run without a live API call. One test per
connection asserts the concrete BadRequestException and the code it carries
from the provider payload. A 400 is used because the SDK does not retry it,
which keeps the exchange to a single request.

Generated-by: Claude Code 2.1.226
@weiqingy
weiqingy force-pushed the 936-unwrap-openai-exceptions branch from b32dac3 to 0a00294 Compare August 12, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants