diff --git a/docs/content/advanced/error-handling.md b/docs/content/advanced/error-handling.md index cbec7c41..87c5d563 100644 --- a/docs/content/advanced/error-handling.md +++ b/docs/content/advanced/error-handling.md @@ -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: @@ -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 @@ -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: diff --git a/docs/content/advanced/spi.md b/docs/content/advanced/spi.md index e2644eea..42a028ba 100644 --- a/docs/content/advanced/spi.md +++ b/docs/content/advanced/spi.md @@ -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` | diff --git a/docs/content/authentication.md b/docs/content/authentication.md index cc13d48f..a6d9d0b2 100644 --- a/docs/content/authentication.md +++ b/docs/content/authentication.md @@ -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 @@ -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 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 ```