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));