Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 55 additions & 12 deletions docs/content/advanced/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ The SDK provides a typed exception hierarchy so you can catch exactly the errors

## Exception hierarchy

All SDK exceptions extend `WatsonxException`, which in turn extends `RuntimeException`. You never need to declare them in `throws` clauses.
API errors are reported through `WatsonxException`, which extends `RuntimeException`. You never need to declare SDK exceptions in `throws` clauses.

```
RuntimeException
└── WatsonxException (base - always has statusCode, errorCode, message, traceId)
├── AuthenticationTokenExpiredException ← handled automatically by the SDK
├── AuthorizationRejectedException
├── InvalidInputArgumentException
├── InvalidRequestEntityException
├── JsonTypeErrorException
├── JsonValidationErrorException
├── ModelNotSupportedException
├── ModelNoSupportForFunctionException
├── TokenQuotaReachedException
└── UserAuthorizationFailedException
├── WatsonxException (base - always has statusCode, errorCode, message, traceId)
│ ├── AuthenticationTokenExpiredException ← handled automatically by the SDK
│ ├── AuthorizationRejectedException
│ ├── InvalidInputArgumentException
│ ├── InvalidRequestEntityException
│ ├── JsonTypeErrorException
│ ├── JsonValidationErrorException
│ ├── ModelNotSupportedException
│ ├── ModelNoSupportForFunctionException
│ ├── TokenQuotaReachedException
│ └── UserAuthorizationFailedException
└── EmptyChatResponseException ← the call succeeded, but there is nothing to read
```

`WatsonxException` exposes:
Expand Down Expand Up @@ -60,6 +61,26 @@ If the API returns an error code that does not map to a specific subclass, the b

---

## Empty chat responses

`EmptyChatResponseException` is not an API error. The request succeeded, but the response carries nothing that can be turned into an `AssistantMessage`, so the message cannot be built. It is thrown by `ChatResponse.toAssistantMessage()` and `ChatResponse.toAssistantMessages()` when:

- the response contains no choices at all
- a choice carries no message
- a choice has no content, no tool calls and no refusal - for example when the model was truncated by `maxCompletionTokens` before emitting anything

Because it extends `RuntimeException` directly and not `WatsonxException`, a `catch (WatsonxException e)` block will **not** catch it.

`EmptyChatResponseException` exposes:

| Method | Type | Description |
|--------|------|-------------|
| `finishReason()` | FinishReason | Finish reason of the empty choice (e.g. `LENGTH`, `TIME_LIMIT`, `CANCELLED`, `ERROR`), or `INCOMPLETE` when the response has no choices |
| `index()` | int | Zero-based index of the empty choice, or `EmptyChatResponseException.NO_CHOICE` (`-1`) when the response has no choices |
| `response()` | ChatResponse | The original response - useful to inspect token usage or log the raw payload |

---

## Usage examples

### Basic error handling
Expand All @@ -85,6 +106,28 @@ try {
}
```

### Handling an empty chat response

`EmptyChatResponseException` is raised when the assistant message is built, not when the request is sent, so catch it around the conversion:

```java
ChatResponse response = chatService.chat("Hello!");

try {
AssistantMessage message = response.toAssistantMessage();
System.out.println(message.content());
} catch (EmptyChatResponseException e) {
switch (e.finishReason()) {
// Truncated before producing output - raise maxCompletionTokens and retry
case LENGTH -> retryWithLargerBudget();
// The request hit the server time limit or was cancelled
case TIME_LIMIT, CANCELLED -> retryLater();
default -> logger.warn("Empty choice {} ({}): {}",
e.index(), e.finishReason(), e.getMessage());
}
}
```

### Handling transient errors

The SDK automatically retries `429`, `503`, `504`, and `520` responses with exponential backoff (see [Environment Variables](./environment-variables)). If retries are exhausted, `WatsonxException` is thrown with the final status code. To implement your own retry on top:
Expand Down
6 changes: 6 additions & 0 deletions docs/content/advanced/spi.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ Every service delegates HTTP communication to an abstract `WatsonxRestClient`, w
| Service | REST client |
|---------|-------------|
| `ChatService` | `ChatRestClient` |
| `TextGenerationService` | `TextGenerationRestClient` |
| `EmbeddingService` | `EmbeddingRestClient` |
| `RerankService` | `RerankRestClient` |
| `TokenizationService` | `TokenizationRestClient` |
| `DetectionService` | `DetectionRestClient` |
| `TextClassificationService` | `TextClassificationRestClient` |
| `TextExtractionService` | `TextExtractionRestClient` |
| `CreateSchemaService` | `CreateSchemaRestClient` |
| `ImproveSchemaService` | `ImproveSchemaRestClient` |
| `MergeSchemaService` | `MergeSchemaRestClient` |
| `ClusterSchemaService` | `ClusterSchemaRestClient` |
| `TimeSeriesService` | `TimeSeriesRestClient` |
| `FoundationModelService` | `FoundationModelRestClient` |
| `ToolService` | `ToolRestClient` |
| `DeploymentService` | `DeploymentRestClient` |
| `ModelGatewayService` | `ModelGatewayRestClient` |
| `ModelGatewayCatalogService` | `ModelGatewayCatalogRestClient` |
| `FileService` | `FileRestClient` |
| `BatchService` | `BatchRestClient` |

Expand Down
48 changes: 48 additions & 0 deletions docs/content/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The SDK uses the `Authenticator` interface as the single abstraction for token-b

Both implementations handle **token caching and automatic renewal** transparently. The SDK fetches a token on the first request, caches it, checks expiry before each subsequent request, and refreshes silently when needed. You never manage token lifecycle manually.

Any other credential source can be plugged in by implementing `Authenticator` yourself. See [Custom Authentication](#custom-authentication).

---

## IBM Cloud Authentication
Expand Down Expand Up @@ -156,6 +158,52 @@ ChatService chatService = ChatService.builder()

---

## Custom Authentication

When the token comes from a source the built-in implementations do not cover, implement the `Authenticator` interface:

```java
import java.util.concurrent.CompletableFuture;
import com.ibm.watsonx.ai.core.auth.Authenticator;

public class MyAuthenticator implements Authenticator {

private final MyTokenProvider tokenProvider;

public MyAuthenticator(MyTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}

@Override
public String token() {
return tokenProvider.accessToken();
}

@Override
public CompletableFuture<String> tokenAsync() {
return CompletableFuture.completedFuture(token());
}

@Override
public String scheme() {
return "Bearer";
}
}
```

Then pass the instance to any service builder through `authenticator(Authenticator)`:

```java
ChatService chatService = ChatService.builder()
.authenticator(new MyAuthenticator(tokenProvider))
.projectId(WATSONX_PROJECT_ID)
.baseUrl(CloudRegion.DALLAS)
.modelId("ibm/granite-4-h-small")
.build();
```

---

## Token lifecycle

```
Expand Down