-
Notifications
You must be signed in to change notification settings - Fork 227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OPIK-547 Store and retrieve LLM provider api key #845
Merged
BorisTkachenko
merged 3 commits into
main
from
boryst/OPIK-547-api-keys-store-and-retrieve
Dec 11, 2024
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,3 +88,6 @@ metadata: | |
|
||
cors: | ||
enabled: ${CORS:-false} | ||
|
||
encryption: | ||
key: ${OPIK_ENCRYPTION_KEY:-'GiTHubiLoVeYouAA'} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
apps/opik-backend/src/main/java/com/comet/opik/api/LlmProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.comet.opik.api; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum LlmProvider { | ||
|
||
@JsonProperty("openai") | ||
OPEN_AI; | ||
} | ||
andrescrz marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKey.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package com.comet.opik.api; | ||
|
||
import com.comet.opik.utils.ProviderApiKeyDeserializer; | ||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.annotation.JsonView; | ||
import com.fasterxml.jackson.databind.PropertyNamingStrategies; | ||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import jakarta.validation.constraints.NotBlank; | ||
import lombok.Builder; | ||
import lombok.NonNull; | ||
|
||
import java.time.Instant; | ||
import java.util.UUID; | ||
|
||
@Builder(toBuilder = true) | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
public record ProviderApiKey( | ||
@JsonView( { | ||
View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) UUID id, | ||
@JsonView({View.Public.class, View.Write.class}) @NonNull LlmProvider provider, | ||
@JsonView({ | ||
View.Write.class}) @NotBlank @JsonDeserialize(using = ProviderApiKeyDeserializer.class) String apiKey, | ||
@JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant createdAt, | ||
@JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String createdBy, | ||
@JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant lastUpdatedAt, | ||
@JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String lastUpdatedBy){ | ||
@Override | ||
public String toString() { | ||
return "ProviderApiKey{" + | ||
"id=" + id + | ||
", provider='" + provider + '\'' + | ||
", createdAt=" + createdAt + | ||
", createdBy='" + createdBy + '\'' + | ||
", lastUpdatedAt=" + lastUpdatedAt + | ||
", lastUpdatedBy='" + lastUpdatedBy + '\'' + | ||
'}'; | ||
} | ||
|
||
public static class View { | ||
public static class Write { | ||
} | ||
|
||
public static class Public { | ||
} | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKeyUpdate.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.comet.opik.api; | ||
|
||
import com.comet.opik.utils.ProviderApiKeyDeserializer; | ||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.databind.PropertyNamingStrategies; | ||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
import jakarta.validation.constraints.NotBlank; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.ToString; | ||
|
||
@Builder(toBuilder = true) | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
@Getter | ||
public class ProviderApiKeyUpdate { | ||
@ToString.Exclude | ||
@NotBlank | ||
@JsonDeserialize(using = ProviderApiKeyDeserializer.class) | ||
String apiKey; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
...backend/src/main/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package com.comet.opik.api.resources.v1.priv; | ||
|
||
import com.codahale.metrics.annotation.Timed; | ||
import com.comet.opik.api.ProviderApiKey; | ||
import com.comet.opik.api.ProviderApiKeyUpdate; | ||
import com.comet.opik.api.error.ErrorMessage; | ||
import com.comet.opik.domain.LlmProviderApiKeyService; | ||
import com.comet.opik.infrastructure.auth.RequestContext; | ||
import com.fasterxml.jackson.annotation.JsonView; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.headers.Header; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import io.swagger.v3.oas.annotations.parameters.RequestBody; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.inject.Inject; | ||
import jakarta.inject.Provider; | ||
import jakarta.validation.Valid; | ||
import jakarta.ws.rs.Consumes; | ||
import jakarta.ws.rs.GET; | ||
import jakarta.ws.rs.PATCH; | ||
import jakarta.ws.rs.POST; | ||
import jakarta.ws.rs.Path; | ||
import jakarta.ws.rs.PathParam; | ||
import jakarta.ws.rs.Produces; | ||
import jakarta.ws.rs.core.Context; | ||
import jakarta.ws.rs.core.MediaType; | ||
import jakarta.ws.rs.core.Response; | ||
import jakarta.ws.rs.core.UriInfo; | ||
import lombok.NonNull; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
import java.util.UUID; | ||
|
||
@Path("/v1/private/llm-provider-key") | ||
@Produces(MediaType.APPLICATION_JSON) | ||
@Consumes(MediaType.APPLICATION_JSON) | ||
@Timed | ||
@Slf4j | ||
@RequiredArgsConstructor(onConstructor_ = @Inject) | ||
@Tag(name = "LlmProviderKey", description = "LLM Provider Key") | ||
public class LlmProviderApiKeyResource { | ||
|
||
private final @NonNull LlmProviderApiKeyService llmProviderApiKeyService; | ||
private final @NonNull Provider<RequestContext> requestContext; | ||
|
||
@GET | ||
@Path("{id}") | ||
@Operation(operationId = "getLlmProviderApiKeyById", summary = "Get LLM Provider's ApiKey by id", description = "Get LLM Provider's ApiKey by id", responses = { | ||
@ApiResponse(responseCode = "200", description = "ProviderApiKey resource", content = @Content(schema = @Schema(implementation = ProviderApiKey.class))), | ||
@ApiResponse(responseCode = "404", description = "Not found", content = @Content(schema = @Schema(implementation = ErrorMessage.class)))}) | ||
@JsonView({ProviderApiKey.View.Public.class}) | ||
public Response getById(@PathParam("id") UUID id) { | ||
|
||
String workspaceId = requestContext.get().getWorkspaceId(); | ||
|
||
log.info("Getting Provider's ApiKey by id '{}' on workspace_id '{}'", id, workspaceId); | ||
|
||
ProviderApiKey providerApiKey = llmProviderApiKeyService.get(id, workspaceId); | ||
|
||
log.info("Got Provider's ApiKey by id '{}' on workspace_id '{}'", id, workspaceId); | ||
|
||
return Response.ok().entity(providerApiKey).build(); | ||
} | ||
|
||
@POST | ||
@Operation(operationId = "storeLlmProviderApiKey", summary = "Store LLM Provider's ApiKey", description = "Store LLM Provider's ApiKey", responses = { | ||
@ApiResponse(responseCode = "201", description = "Created", headers = { | ||
@Header(name = "Location", required = true, example = "${basePath}/v1/private/proxy/api_key/{apiKeyId}", schema = @Schema(implementation = String.class))}), | ||
@ApiResponse(responseCode = "401", description = "Bad Request", content = @Content(schema = @Schema(implementation = ErrorMessage.class))), | ||
@ApiResponse(responseCode = "403", description = "Access forbidden", content = @Content(schema = @Schema(implementation = ErrorMessage.class))) | ||
}) | ||
public Response saveApiKey( | ||
@RequestBody(content = @Content(schema = @Schema(implementation = ProviderApiKey.class))) @JsonView(ProviderApiKey.View.Write.class) @Valid ProviderApiKey providerApiKey, | ||
@Context UriInfo uriInfo) { | ||
String workspaceId = requestContext.get().getWorkspaceId(); | ||
String userName = requestContext.get().getUserName(); | ||
log.info("Save api key for provider '{}', on workspace_id '{}'", providerApiKey.provider(), workspaceId); | ||
var providerApiKeyId = llmProviderApiKeyService.saveApiKey(providerApiKey, userName, workspaceId).id(); | ||
log.info("Saved api key for provider '{}', on workspace_id '{}'", providerApiKey.provider(), workspaceId); | ||
|
||
var uri = uriInfo.getAbsolutePathBuilder().path("/%s".formatted(providerApiKeyId)).build(); | ||
|
||
return Response.created(uri).build(); | ||
} | ||
|
||
@PATCH | ||
@Path("{id}") | ||
@Operation(operationId = "updateLlmProviderApiKey", summary = "Update LLM Provider's ApiKey", description = "Update LLM Provider's ApiKey", responses = { | ||
@ApiResponse(responseCode = "204", description = "No Content"), | ||
@ApiResponse(responseCode = "401", description = "Bad Request", content = @Content(schema = @Schema(implementation = ErrorMessage.class))), | ||
@ApiResponse(responseCode = "403", description = "Access forbidden", content = @Content(schema = @Schema(implementation = ErrorMessage.class))), | ||
@ApiResponse(responseCode = "404", description = "Not found", content = @Content(schema = @Schema(implementation = ErrorMessage.class))) | ||
}) | ||
public Response updateApiKey(@PathParam("id") UUID id, | ||
@RequestBody(content = @Content(schema = @Schema(implementation = ProviderApiKeyUpdate.class))) @Valid ProviderApiKeyUpdate providerApiKeyUpdate) { | ||
String workspaceId = requestContext.get().getWorkspaceId(); | ||
String userName = requestContext.get().getUserName(); | ||
|
||
log.info("Updating api key for provider with id '{}' on workspaceId '{}'", id, workspaceId); | ||
llmProviderApiKeyService.updateApiKey(id, providerApiKeyUpdate, userName, workspaceId); | ||
log.info("Updated api key for provider with id '{}' on workspaceId '{}'", id, workspaceId); | ||
|
||
return Response.noContent().build(); | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyDAO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.comet.opik.domain; | ||
|
||
import com.comet.opik.api.ProviderApiKey; | ||
import com.comet.opik.infrastructure.db.UUIDArgumentFactory; | ||
import org.jdbi.v3.sqlobject.config.RegisterArgumentFactory; | ||
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; | ||
import org.jdbi.v3.sqlobject.customizer.Bind; | ||
import org.jdbi.v3.sqlobject.customizer.BindMethods; | ||
import org.jdbi.v3.sqlobject.statement.SqlQuery; | ||
import org.jdbi.v3.sqlobject.statement.SqlUpdate; | ||
|
||
import java.util.Optional; | ||
import java.util.UUID; | ||
|
||
@RegisterConstructorMapper(ProviderApiKey.class) | ||
@RegisterArgumentFactory(UUIDArgumentFactory.class) | ||
public interface LlmProviderApiKeyDAO { | ||
|
||
@SqlUpdate("INSERT INTO llm_provider_api_key (id, provider, workspace_id, api_key, created_by, last_updated_by) VALUES (:bean.id, :bean.provider, :workspaceId, :bean.apiKey, :bean.createdBy, :bean.lastUpdatedBy)") | ||
void save(@Bind("workspaceId") String workspaceId, | ||
@BindMethods("bean") ProviderApiKey providerApiKey); | ||
|
||
@SqlUpdate("UPDATE llm_provider_api_key SET " + | ||
"api_key = :apiKey, " + | ||
"last_updated_by = :lastUpdatedBy " + | ||
"WHERE id = :id AND workspace_id = :workspaceId") | ||
void update(@Bind("id") UUID id, | ||
@Bind("workspaceId") String workspaceId, | ||
@Bind("apiKey") String encryptedApiKey, | ||
@Bind("lastUpdatedBy") String lastUpdatedBy); | ||
|
||
@SqlQuery("SELECT * FROM llm_provider_api_key WHERE id = :id AND workspace_id = :workspaceId") | ||
ProviderApiKey findById(@Bind("id") UUID id, @Bind("workspaceId") String workspaceId); | ||
|
||
default Optional<ProviderApiKey> fetch(UUID id, String workspaceId) { | ||
return Optional.ofNullable(findById(id, workspaceId)); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need a module since this will be static. We can just set the configuration using the
run
method below.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it could be set in run. Since we are using DI this approach seems more consistent. Can be changed, don't have any preferences here.