diff --git a/README.md b/README.md index 4523e19..a3b441a 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ ChatCompletion chatCompletion = chatClient.createChatCompletion(createChatComple | [Projects](https://platform.openai.com/docs/api-reference/projects) | ✔️ | | [Project Users](https://platform.openai.com/docs/api-reference/project-users) | ✔️ | | [Project Service Accounts](https://platform.openai.com/docs/api-reference/project-service-accounts) | ✔️ | -| [Project API Keys](https://platform.openai.com/docs/api-reference/project-api-keys) | | +| [Project API Keys](https://platform.openai.com/docs/api-reference/project-api-keys) | ✔️ | | [Audit Logs](https://platform.openai.com/docs/api-reference/audit-logs) | | > **_NOTE:_** Legacy APIs are not supported diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java index 3705987..06dc8c0 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java @@ -8,8 +8,8 @@ import java.util.Optional; /** - * A class which when created using the {@link #newBuilder(String)} can be used to create clients - * based on the endpoints defined at API + * A class which when created using the {@link OpenAI.Builder} can be used to create clients based + * on the endpoints defined at API * Reference - OpenAI API */ public final class OpenAI { @@ -37,6 +37,7 @@ public final class OpenAI { private final ProjectsClient projectsClient; private final ProjectUsersClient projectUsersClient; private final ProjectServiceAccountsClient projectServiceAccountsClient; + private final ProjectApiKeysClient projectApiKeysClient; private OpenAI( URI baseUrl, @@ -86,6 +87,8 @@ private OpenAI( projectServiceAccountsClient = new ProjectServiceAccountsClient( baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout); + projectApiKeysClient = + new ProjectApiKeysClient(baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout); } /** @@ -276,6 +279,14 @@ public ProjectServiceAccountsClient projectServiceAccountsClient() { return projectServiceAccountsClient; } + /** + * @return a client based on Project API Keys + */ + public ProjectApiKeysClient projectApiKeysClient() { + return projectApiKeysClient; + } + private String[] createAuthenticationHeaders( Optional apiKey, Optional organization, Optional project) { List authHeaders = new ArrayList<>(); diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectApiKey.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectApiKey.java new file mode 100644 index 0000000..aeb92d2 --- /dev/null +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectApiKey.java @@ -0,0 +1,8 @@ +package io.github.stefanbratanov.jvm.openai; + +/** Represents an individual API key in a project. */ +public record ProjectApiKey( + String redactedValue, String name, long createdAt, String id, Owner owner) { + + public record Owner(String type, ProjectUser user, ProjectServiceAccount serviceAccount) {} +} diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectApiKeysClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectApiKeysClient.java new file mode 100644 index 0000000..26af58a --- /dev/null +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectApiKeysClient.java @@ -0,0 +1,103 @@ +package io.github.stefanbratanov.jvm.openai; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Manage API keys for a given project. Supports listing and deleting keys for users. This API does + * not allow issuing keys for users, as users need to authorize themselves to generate keys. + * + *

Based on Project API + * Keys + */ +public final class ProjectApiKeysClient extends OpenAIClient { + + private static final String API_KEYS_SEGMENT = "/api_keys"; + + private final URI baseUrl; + + ProjectApiKeysClient( + URI baseUrl, + String[] authenticationHeaders, + HttpClient httpClient, + Optional requestTimeout) { + super(authenticationHeaders, httpClient, requestTimeout); + this.baseUrl = baseUrl; + } + + /** + * Returns a list of API keys in the project. + * + * @param projectId The ID of the project. + * @param after A cursor for use in pagination. after is an object ID that defines your place in + * the list. + * @param limit A limit on the number of objects to be returned. + * @throws OpenAIException in case of API errors + */ + public PaginatedProjectApiKeys listProjectApiKeys( + String projectId, Optional after, Optional limit) { + String queryParameters = + createQueryParameters( + Map.of(Constants.LIMIT_QUERY_PARAMETER, limit, Constants.AFTER_QUERY_PARAMETER, after)); + HttpRequest httpRequest = + newHttpRequestBuilder() + .uri( + baseUrl.resolve( + Endpoint.PROJECTS.getPath() + + "/" + + projectId + + API_KEYS_SEGMENT + + queryParameters)) + .GET() + .build(); + HttpResponse httpResponse = sendHttpRequest(httpRequest); + return deserializeResponse(httpResponse.body(), PaginatedProjectApiKeys.class); + } + + public record PaginatedProjectApiKeys( + List data, String firstId, String lastId, boolean hasMore) {} + + /** + * Retrieves an API key in the project. + * + * @param projectId The ID of the project. + * @param keyId The ID of the API key. + * @throws OpenAIException in case of API errors + */ + public ProjectApiKey retrieveProjectApiKey(String projectId, String keyId) { + HttpRequest httpRequest = + newHttpRequestBuilder() + .uri( + baseUrl.resolve( + Endpoint.PROJECTS.getPath() + "/" + projectId + API_KEYS_SEGMENT + "/" + keyId)) + .GET() + .build(); + HttpResponse httpResponse = sendHttpRequest(httpRequest); + return deserializeResponse(httpResponse.body(), ProjectApiKey.class); + } + + /** + * Deletes an API key from the project. + * + * @param projectId The ID of the project. + * @param keyId The ID of the API key. + * @throws OpenAIException in case of API errors + */ + public DeletionStatus deleteProjectApiKey(String projectId, String keyId) { + HttpRequest httpRequest = + newHttpRequestBuilder() + .uri( + baseUrl.resolve( + Endpoint.PROJECTS.getPath() + "/" + projectId + API_KEYS_SEGMENT + "/" + keyId)) + .DELETE() + .build(); + HttpResponse httpResponse = sendHttpRequest(httpRequest); + return deserializeResponse(httpResponse.body(), DeletionStatus.class); + } +} diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectServiceAccountsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectServiceAccountsClient.java index fb426cb..849c2e6 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectServiceAccountsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ProjectServiceAccountsClient.java @@ -16,8 +16,8 @@ * deleted from a project. * *

Based on Project - * Service Accounts + * href="https://platform.openai.com/docs/api-reference/project-service-accounts">Project Service + * Accounts */ public final class ProjectServiceAccountsClient extends OpenAIClient { @@ -37,6 +37,7 @@ public final class ProjectServiceAccountsClient extends OpenAIClient { /** * Returns a list of service accounts in the project. * + * @param projectId The ID of the project. * @param after A cursor for use in pagination. after is an object ID that defines your place in * the list. * @param limit A limit on the number of objects to be returned. diff --git a/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIAdminIntegrationTest.java b/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIAdminIntegrationTest.java index 86d8c6f..18e87b5 100644 --- a/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIAdminIntegrationTest.java +++ b/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIAdminIntegrationTest.java @@ -140,6 +140,27 @@ void testProjectServiceAccountsClient() { assertThat(deletionStatus.deleted()).isTrue(); } + @Test + void testProjectApiKeysClient() { + ProjectApiKeysClient projectApiKeysClient = openAI.projectApiKeysClient(); + + Project project = retrieveProject(); + + List projectApiKeys = + projectApiKeysClient + .listProjectApiKeys(project.id(), Optional.empty(), Optional.empty()) + .data(); + + assertThat(projectApiKeys).isNotEmpty(); + + ProjectApiKey projectApiKey = projectApiKeys.get(0); + + ProjectApiKey retrievedProjectApiKey = + projectApiKeysClient.retrieveProjectApiKey(project.id(), projectApiKey.id()); + + assertThat(retrievedProjectApiKey).isEqualTo(projectApiKey); + } + private Project retrieveProject() { return openAI .projectsClient() diff --git a/src/test/java/io/github/stefanbratanov/jvm/openai/OpenApiSpecificationValidationTest.java b/src/test/java/io/github/stefanbratanov/jvm/openai/OpenApiSpecificationValidationTest.java index 4672982..36219a6 100644 --- a/src/test/java/io/github/stefanbratanov/jvm/openai/OpenApiSpecificationValidationTest.java +++ b/src/test/java/io/github/stefanbratanov/jvm/openai/OpenApiSpecificationValidationTest.java @@ -12,6 +12,7 @@ import com.atlassian.oai.validator.report.ValidationReport; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.stefanbratanov.jvm.openai.ProjectApiKeysClient.PaginatedProjectApiKeys; import io.github.stefanbratanov.jvm.openai.ProjectServiceAccountsClient.ProjectServiceAccountCreateResponse; import io.github.stefanbratanov.jvm.openai.RunStepsClient.PaginatedThreadRunSteps; import io.swagger.v3.oas.models.OpenAPI; @@ -526,6 +527,20 @@ void validateProjectServiceAccounts() { validate(request, response); } + @RepeatedTest(25) + void validateProjectApiKeys() { + Request request = + new SimpleRequest.Builder( + Method.GET, "/" + Endpoint.PROJECTS.getPath() + "/{project_id}/api_keys") + .build(); + + PaginatedProjectApiKeys paginatedProjectApiKeys = testDataUtil.randomPaginatedProjectApiKeys(); + + Response response = createResponseWithBody(serializeObject(paginatedProjectApiKeys)); + + validate(request, response); + } + private void validate(Request request, Response response, String... reportMessagesToIgnore) { ValidationReport report = validator.validate(request, response); validateReport(report, reportMessagesToIgnore); diff --git a/src/test/java/io/github/stefanbratanov/jvm/openai/TestDataUtil.java b/src/test/java/io/github/stefanbratanov/jvm/openai/TestDataUtil.java index fe0fdcc..842d94e 100644 --- a/src/test/java/io/github/stefanbratanov/jvm/openai/TestDataUtil.java +++ b/src/test/java/io/github/stefanbratanov/jvm/openai/TestDataUtil.java @@ -2,6 +2,8 @@ import io.github.stefanbratanov.jvm.openai.CreateChatCompletionRequest.StreamOptions; import io.github.stefanbratanov.jvm.openai.FineTuningJobIntegration.Wandb; +import io.github.stefanbratanov.jvm.openai.ProjectApiKey.Owner; +import io.github.stefanbratanov.jvm.openai.ProjectApiKeysClient.PaginatedProjectApiKeys; import io.github.stefanbratanov.jvm.openai.ProjectServiceAccountsClient.ApiKey; import io.github.stefanbratanov.jvm.openai.ProjectServiceAccountsClient.ProjectServiceAccountCreateResponse; import io.github.stefanbratanov.jvm.openai.RunStepsClient.PaginatedThreadRunSteps; @@ -791,6 +793,24 @@ public ProjectServiceAccountCreateResponse randomProjectServiceAccountCreateResp randomString(5), randomString(7), randomString(12), randomLong(9999, 1_000_000))); } + public PaginatedProjectApiKeys randomPaginatedProjectApiKeys() { + return new PaginatedProjectApiKeys( + listOf(randomInt(1, 5), this::randomProjectApiKey), + randomString(5), + randomString(5), + randomBoolean()); + } + + private ProjectApiKey randomProjectApiKey() { + return new ProjectApiKey( + randomString(8), + randomString(5), + randomLong(10_000, 99_999), + randomString(6), + new Owner( + oneOf("user", "service_account"), randomProjectUser(), randomProjectServiceAccount())); + } + private ProjectServiceAccount randomProjectServiceAccount() { return new ProjectServiceAccount( randomString(5), randomString(7), "member", randomLong(10_000, 99_999));