diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreChatCompletionService.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreChatCompletionService.java new file mode 100644 index 000000000..afd307be1 --- /dev/null +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreChatCompletionService.java @@ -0,0 +1,174 @@ +package com.sap.ai.sdk.foundationmodels.openai; + +import com.openai.core.ClientOptions; +import com.openai.core.RequestOptions; +import com.openai.core.http.StreamResponse; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionChunk; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import com.openai.models.chat.completions.ChatCompletionDeleteParams; +import com.openai.models.chat.completions.ChatCompletionDeleted; +import com.openai.models.chat.completions.ChatCompletionListPage; +import com.openai.models.chat.completions.ChatCompletionListParams; +import com.openai.models.chat.completions.ChatCompletionRetrieveParams; +import com.openai.models.chat.completions.ChatCompletionUpdateParams; +import com.openai.models.chat.completions.StructuredChatCompletion; +import com.openai.models.chat.completions.StructuredChatCompletionCreateParams; +import com.openai.services.blocking.chat.ChatCompletionService; +import com.openai.services.blocking.chat.completions.MessageService; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +class AiCoreChatCompletionService implements ChatCompletionService { + + @Delegate(types = PassThroughMethods.class) + private final ChatCompletionService delegate; + + private final String deploymentModel; + + @Override + @Nonnull + public ChatCompletionService withOptions( + @Nonnull final Consumer consumer) { + return new AiCoreChatCompletionService(delegate.withOptions(consumer), deploymentModel); + } + + @Override + @Nonnull + public ChatCompletionService.WithRawResponse withRawResponse() { + throw new UnsupportedOperationException( + "withRawResponse() is not supported by AiCoreResponseService."); + } + + @Override + @Nonnull + public ChatCompletion create(@Nonnull final ChatCompletionCreateParams params) { + return create(params, RequestOptions.none()); + } + + @Override + @Nonnull + public ChatCompletion create( + @Nonnull final ChatCompletionCreateParams params, + @Nonnull final RequestOptions requestOptions) { + throwOnModelMismatch(params.model().asString()); + return delegate.create(params, requestOptions); + } + + @Override + @Nonnull + public StructuredChatCompletion create( + @Nonnull final StructuredChatCompletionCreateParams params) { + return create(params, RequestOptions.none()); + } + + @Override + @Nonnull + public StructuredChatCompletion create( + @Nonnull final StructuredChatCompletionCreateParams params, + @Nonnull final RequestOptions requestOptions) { + throwOnModelMismatch(params.rawParams().model().asString()); + return delegate.create(params, requestOptions); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final ChatCompletionCreateParams params) { + return createStreaming(params, RequestOptions.none()); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final ChatCompletionCreateParams params, + @Nonnull final RequestOptions requestOptions) { + throwOnModelMismatch(params.model().asString()); + return delegate.createStreaming(params, requestOptions); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final StructuredChatCompletionCreateParams params) { + return createStreaming(params, RequestOptions.none()); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final StructuredChatCompletionCreateParams params, + @Nonnull final RequestOptions requestOptions) { + throwOnModelMismatch(params.rawParams().model().asString()); + return delegate.createStreaming(params, requestOptions); + } + + private void throwOnModelMismatch(@Nonnull final String givenModel) { + if (!deploymentModel.equals(givenModel)) { + throw new IllegalArgumentException( + """ + Model mismatch: + Expected : '%s' (configured via forModel()) + Actual : '%s' (set in request parameters) + Fix: Either remove the model from the request parameters, \ + or use forModel("%s") when creating the client.\ + """ + .formatted(deploymentModel, givenModel, givenModel)); + } + } + + private interface PassThroughMethods { + ChatCompletionDeleted delete( + ChatCompletionDeleteParams chatCompletionDeleteParams, RequestOptions requestOptions); + + ChatCompletionDeleted delete(String completionId); + + ChatCompletionDeleted delete(String completionId, ChatCompletionDeleteParams params); + + ChatCompletionDeleted delete( + String completionId, ChatCompletionDeleteParams params, RequestOptions requestOptions); + + ChatCompletionDeleted delete(String completionId, RequestOptions requestOptions); + + ChatCompletionDeleted delete(ChatCompletionDeleteParams params); + + ChatCompletionListPage list(); + + ChatCompletionListPage list( + ChatCompletionListParams chatCompletionListParams, RequestOptions requestOptions); + + ChatCompletionListPage list(ChatCompletionListParams params); + + ChatCompletionListPage list(RequestOptions requestOptions); + + MessageService messages(); + + ChatCompletion retrieve( + ChatCompletionRetrieveParams chatCompletionRetrieveParams, RequestOptions requestOptions); + + ChatCompletion retrieve(String completionId); + + ChatCompletion retrieve(String completionId, ChatCompletionRetrieveParams params); + + ChatCompletion retrieve( + String completionId, ChatCompletionRetrieveParams params, RequestOptions requestOptions); + + ChatCompletion retrieve(String completionId, RequestOptions requestOptions); + + ChatCompletion retrieve(ChatCompletionRetrieveParams params); + + ChatCompletion update( + ChatCompletionUpdateParams chatCompletionUpdateParams, RequestOptions requestOptions); + + ChatCompletion update(String completionId, ChatCompletionUpdateParams params); + + ChatCompletion update( + String completionId, ChatCompletionUpdateParams params, RequestOptions requestOptions); + + ChatCompletion update(ChatCompletionUpdateParams params); + } +} diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreChatService.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreChatService.java new file mode 100644 index 000000000..641255c47 --- /dev/null +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreChatService.java @@ -0,0 +1,40 @@ +package com.sap.ai.sdk.foundationmodels.openai; + +import com.openai.core.ClientOptions; +import com.openai.core.http.QueryParams; +import com.openai.services.blocking.ChatService; +import com.openai.services.blocking.chat.ChatCompletionService; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +class AiCoreChatService implements ChatService { + + @Delegate private final ChatService delegate; + private final String deploymentModel; + + @Override + @Nonnull + public ChatService withOptions(@Nonnull final Consumer consumer) { + return new AiCoreChatService(delegate.withOptions(consumer), deploymentModel); + } + + @Override + @Nonnull + public WithRawResponse withRawResponse() { + throw new UnsupportedOperationException( + "withRawResponse() is not supported for AiCoreChatService"); + } + + @Override + @Nonnull + public ChatCompletionService completions() { + final var apiVersionQuery = QueryParams.builder().put("api-version", "2024-02-01").build(); + final var completions = + delegate.completions().withOptions(builder -> builder.queryParams(apiVersionQuery)); + return new AiCoreChatCompletionService(completions, deploymentModel); + } +} diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java index ce4bdf487..5d1d97459 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java @@ -10,6 +10,7 @@ import com.openai.core.http.HttpRequest; import com.openai.core.http.HttpResponse; import com.openai.errors.OpenAIIoException; +import com.openai.models.ChatModel; import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.core.DeploymentResolutionException; import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; @@ -79,26 +80,31 @@ public static OpenAIClient forModel( @Nonnull final OpenAiModel model, @Nonnull final String resourceGroup) { final HttpDestination destination = new AiCoreService().getInferenceDestination(resourceGroup).forModel(model); - - return fromDestination(destination); + return fromDestination(destination, model); } /** * Create an OpenAI client from a pre-resolved destination. * * @param destination The destination to use. + * @param model The model name for validation. * @return A configured OpenAI client instance. */ @Nonnull @SuppressWarnings("PMD.CloseResource") - static OpenAIClient fromDestination(@Nonnull final HttpDestination destination) { + static OpenAIClient fromDestination( + @Nonnull final HttpDestination destination, @Nonnull final OpenAiModel model) { final var baseUrl = destination.getUri().toString(); final var httpClient = new AiCoreHttpClientImpl(destination); - final ClientOptions clientOptions = + final var clientOptions = ClientOptions.builder().baseUrl(baseUrl).httpClient(httpClient).apiKey("unused").build(); - - return new OpenAIClientImpl(clientOptions); + final var chatModel = + Optional.ofNullable(model.version()) + .map(version -> model.name() + "-" + version) + .orElseGet(model::name); + return new OpenAIClientImplWrapper( + new OpenAIClientImpl(clientOptions), ChatModel.of(chatModel).asString()); } /** diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreResponseService.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreResponseService.java new file mode 100644 index 000000000..090aa639a --- /dev/null +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreResponseService.java @@ -0,0 +1,186 @@ +package com.sap.ai.sdk.foundationmodels.openai; + +import com.openai.core.ClientOptions; +import com.openai.core.RequestOptions; +import com.openai.core.http.StreamResponse; +import com.openai.models.ChatModel; +import com.openai.models.ResponsesModel; +import com.openai.models.responses.CompactedResponse; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCancelParams; +import com.openai.models.responses.ResponseCompactParams; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseDeleteParams; +import com.openai.models.responses.ResponseRetrieveParams; +import com.openai.models.responses.ResponseStreamEvent; +import com.openai.models.responses.StructuredResponse; +import com.openai.models.responses.StructuredResponseCreateParams; +import com.openai.services.blocking.ResponseService; +import com.openai.services.blocking.responses.InputItemService; +import com.openai.services.blocking.responses.InputTokenService; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +class AiCoreResponseService implements ResponseService { + + @Delegate(types = PassThroughMethods.class) + private final ResponseService delegate; + + private final String deploymentModel; + + @Override + @Nonnull + public ResponseService withOptions(@Nonnull final Consumer modifier) { + return new AiCoreResponseService(delegate.withOptions(modifier), deploymentModel); + } + + @Override + @Nonnull + public ResponseService.WithRawResponse withRawResponse() { + throw new UnsupportedOperationException( + "withRawResponse() is not supported by AiCoreResponseService."); + } + + @Override + @Nonnull + public Response create(@Nonnull final ResponseCreateParams params) { + return create(params, RequestOptions.none()); + } + + @Override + @Nonnull + public Response create( + @Nonnull final ResponseCreateParams params, @Nonnull final RequestOptions requestOptions) { + return delegate.create(useDeploymentModel(params), requestOptions); + } + + @Override + @Nonnull + public StructuredResponse create(@Nonnull final StructuredResponseCreateParams params) { + return create(params, RequestOptions.none()); + } + + @Override + @Nonnull + public StructuredResponse create( + @Nonnull final StructuredResponseCreateParams params, + @Nonnull final RequestOptions requestOptions) { + return delegate.create(useDeploymentModel(params), requestOptions); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final ResponseCreateParams params) { + return createStreaming(params, RequestOptions.none()); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final ResponseCreateParams params, @Nonnull final RequestOptions requestOptions) { + return delegate.createStreaming(useDeploymentModel(params), requestOptions); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final StructuredResponseCreateParams params) { + return createStreaming(params, RequestOptions.none()); + } + + @Override + @Nonnull + public StreamResponse createStreaming( + @Nonnull final StructuredResponseCreateParams params, + @Nonnull final RequestOptions requestOptions) { + return delegate.createStreaming(useDeploymentModel(params), requestOptions); + } + + @Override + @Nonnull + public CompactedResponse compact(@Nonnull final ResponseCompactParams params) { + return compact(params, RequestOptions.none()); + } + + @Override + @Nonnull + public CompactedResponse compact( + @Nonnull final ResponseCompactParams params, @Nonnull final RequestOptions requestOptions) { + return delegate.compact(useDeploymentModel(params), requestOptions); + } + + @Nonnull + private StructuredResponseCreateParams useDeploymentModel( + @Nonnull final StructuredResponseCreateParams params) { + final ResponseCreateParams validated = useDeploymentModel(params.rawParams()); + return new StructuredResponseCreateParams<>(params.responseType(), validated); + } + + @Nonnull + private ResponseCreateParams useDeploymentModel(@Nonnull final ResponseCreateParams params) { + final var givenModel = + params.model().map(ResponsesModel::asChat).map(ChatModel::asString).orElse(null); + + if (givenModel == null) { + return params.toBuilder().model(deploymentModel).build(); + } + + throwOnModelMismatch(givenModel); + return params; + } + + @Nonnull + private ResponseCompactParams useDeploymentModel(@Nonnull final ResponseCompactParams params) { + final String givenModel = + params.model().map(ResponseCompactParams.Model::asString).orElse(null); + + if (givenModel == null) { + return params.toBuilder().model(deploymentModel).build(); + } + + throwOnModelMismatch(givenModel); + return params; + } + + private void throwOnModelMismatch(@Nonnull final String givenModel) { + if (!deploymentModel.equals(givenModel)) { + throw new IllegalArgumentException( + """ + Model mismatch: + Expected : '%s' (configured via forModel()) + Actual : '%s' (set in request parameters) + Fix: Either remove the model from the request parameters, \ + or use forModel("%s") when creating the client.\ + """ + .formatted(deploymentModel, givenModel, givenModel)); + } + } + + private interface PassThroughMethods { + InputItemService inputItems(); + + InputTokenService inputTokens(); + + Response retrieve(ResponseRetrieveParams params, RequestOptions requestOptions); + + Response retrieve(ResponseRetrieveParams params); + + StreamResponse retrieveStreaming( + ResponseRetrieveParams params, RequestOptions requestOptions); + + StreamResponse retrieveStreaming(ResponseRetrieveParams params); + + void delete(ResponseDeleteParams params, RequestOptions requestOptions); + + void delete(ResponseDeleteParams params); + + Response cancel(ResponseCancelParams params, RequestOptions requestOptions); + + Response cancel(ResponseCancelParams params); + } +} diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAIClientImplWrapper.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAIClientImplWrapper.java new file mode 100644 index 000000000..9308f19f6 --- /dev/null +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAIClientImplWrapper.java @@ -0,0 +1,120 @@ +package com.sap.ai.sdk.foundationmodels.openai; + +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientAsync; +import com.openai.client.OpenAIClientImpl; +import com.openai.core.ClientOptions; +import com.openai.services.blocking.AudioService; +import com.openai.services.blocking.BatchService; +import com.openai.services.blocking.BetaService; +import com.openai.services.blocking.ChatService; +import com.openai.services.blocking.CompletionService; +import com.openai.services.blocking.ContainerService; +import com.openai.services.blocking.ConversationService; +import com.openai.services.blocking.EmbeddingService; +import com.openai.services.blocking.EvalService; +import com.openai.services.blocking.FileService; +import com.openai.services.blocking.FineTuningService; +import com.openai.services.blocking.GraderService; +import com.openai.services.blocking.ImageService; +import com.openai.services.blocking.ModelService; +import com.openai.services.blocking.ModerationService; +import com.openai.services.blocking.RealtimeService; +import com.openai.services.blocking.ResponseService; +import com.openai.services.blocking.SkillService; +import com.openai.services.blocking.UploadService; +import com.openai.services.blocking.VectorStoreService; +import com.openai.services.blocking.VideoService; +import com.openai.services.blocking.WebhookService; +import com.sap.ai.sdk.foundationmodels.openai.AiCoreOpenAiClient.AiCoreHttpClientImpl; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; + +@RequiredArgsConstructor +class OpenAIClientImplWrapper implements OpenAIClient { + + @Delegate(types = OtherMethods.class) + @Nonnull + private final OpenAIClientImpl delegate; + + @Nonnull private final String deploymentModel; + + @Nonnull + @Override + public OpenAIClient withOptions(@Nonnull final Consumer consumer) { + return new OpenAIClientImplWrapper( + (OpenAIClientImpl) delegate.withOptions(consumer), deploymentModel); + } + + @Nonnull + @Override + public ChatService chat() { + return new AiCoreChatService(delegate.chat(), deploymentModel); + } + + @Override + @Nonnull + public ResponseService responses() { + return new AiCoreResponseService(delegate.responses(), deploymentModel); + } + + /** + * Methods that are delegated to the underlying OpenAI client. + * + *

Note: Most of these methods will throw {@link UnsupportedOperationException} at runtime due + * to endpoint constraints enforced in {@code AiCoreHttpClientImpl}. + * + * @see AiCoreHttpClientImpl#validateAllowedEndpoint + */ + private interface OtherMethods { + OpenAIClientAsync async(); + + OpenAIClient.WithRawResponse withRawResponse(); + + OpenAIClient withOptions(Consumer modifier); + + CompletionService completions(); + + EmbeddingService embeddings(); + + FileService files(); + + ImageService images(); + + AudioService audio(); + + ModerationService moderations(); + + ModelService models(); + + FineTuningService fineTuning(); + + GraderService graders(); + + VectorStoreService vectorStores(); + + WebhookService webhooks(); + + BetaService beta(); + + BatchService batches(); + + UploadService uploads(); + + RealtimeService realtime(); + + ConversationService conversations(); + + EvalService evals(); + + ContainerService containers(); + + SkillService skills(); + + VideoService videos(); + + void close(); + } +} diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java index d2741ee34..9a3e8667e 100644 --- a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java +++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java @@ -1,11 +1,11 @@ package com.sap.ai.sdk.foundationmodels.openai; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.openai.client.OpenAIClient; -import com.openai.core.http.QueryParams; import com.openai.models.ChatModel; import com.openai.models.chat.completions.ChatCompletion; import com.openai.models.chat.completions.ChatCompletionCreateParams; @@ -15,9 +15,11 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Cache; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @WireMockTest @@ -28,7 +30,7 @@ class AiCoreOpenAiClientTest { @BeforeEach void setup(@Nonnull final WireMockRuntimeInfo server) { final var destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); - client = AiCoreOpenAiClient.fromDestination(destination); + client = AiCoreOpenAiClient.fromDestination(destination, OpenAiModel.GPT_5); ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED); } @@ -53,16 +55,72 @@ void testResponseServiceSuccessWithMatchingModel() { assertThat(response.status().orElseThrow()).isEqualTo(ResponseStatus.COMPLETED); } + @Test + void testResponseServiceSuccessWithoutModel() { + final var params = + ResponseCreateParams.builder().input("What is the capital of France?").build(); + final Response response = client.responses().create(params); + + assertThat(response).isNotNull(); + assertThat(response.status().orElseThrow()).isEqualTo(ResponseStatus.COMPLETED); + } + + @Test + void testResponseServiceFailsWithModelMismatch() { + final var params = + ResponseCreateParams.builder() + .input("What is the capital of France?") + .model(ChatModel.GPT_4) // Different from client's expected model "gpt-5" + .build(); + + assertThatThrownBy(() -> client.responses().create(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Model mismatch") + .hasMessageContaining("gpt-5") + .hasMessageContaining("gpt-4"); + } + + @Test + void testResponseServiceWithOptions() { + final var service = client.responses(); + + final var modifiedService = + service.withOptions( + builder -> { + // Modify some option + builder.putHeader("X-Custom-Header", "test-value"); + }); + + assertThat(modifiedService).isInstanceOf(AiCoreResponseService.class); + } + @Test void testChatCompletionServiceSuccessWithMatchingModel() { final var params = ChatCompletionCreateParams.builder() .model(ChatModel.GPT_5) .addUserMessage("Say this is a test") - .additionalQueryParams(QueryParams.builder().put("api-version", "2024-02-01").build()) .build(); final ChatCompletion response = client.chat().completions().create(params); assertThat(response).isNotNull(); } + + @Test + @Disabled("Fails as Async client needs additional wrappers. Maintenance wall.") + void testAsyncChatCompletion() { + final var params = + ChatCompletionCreateParams.builder() + .model(ChatModel.GPT_5) + .addUserMessage("Say this is a test") + .build(); + + final CompletableFuture future = + client.async().chat().completions().create(params); + + final ChatCompletion response = future.join(); + + assertThat(response).isNotNull(); + assertThat(response.choices()).isNotEmpty(); + } }