diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 28b94633b..1528138a7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -130,7 +130,7 @@ jobs: with: # The companion workflow parses this artifact as untrusted data only. name: codeql-pr-results-${{ matrix.language }} - path: results/${{ matrix.language }}.sarif + path: results/*.sarif if-no-files-found: error retention-days: 1 diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/function/management/dto/PriorityDto.java b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/function/management/dto/PriorityDto.java index 0496c7ba7..d3cfbab87 100644 --- a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/function/management/dto/PriorityDto.java +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/function/management/dto/PriorityDto.java @@ -31,6 +31,12 @@ import java.lang.annotation.Target; import java.util.Map; import org.springframework.util.CollectionUtils; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.deser.std.StdScalarDeserializer; +import tools.jackson.databind.type.LogicalType; @PriorityDto.ValidPriority @Schema(types = {"object"}, @@ -40,6 +46,7 @@ public record PriorityDto( @Min(value = 0, message = "defaultPriority must be >= 0") @Max(value = MAX_PRIORITY, message = "defaultPriority must be <= " + MAX_PRIORITY) @Schema(description = "Default priority.") + @JsonDeserialize(using = PriorityValueDeserializer.class) Long defaultPriority, @Nullable @@ -47,6 +54,7 @@ public record PriorityDto( message = "Maximum number of perAccountPriority entries of " + MAX_PER_ACCOUNT_ENTRIES + " is exceeded.") @Schema(description = "Per-account priority overrides, keyed by account ID.") + @JsonDeserialize(contentUsing = PriorityValueDeserializer.class) Map { + public PriorityValueDeserializer() { + super(Long.class); + } + + @Override + public LogicalType logicalType() { + return LogicalType.Integer; + } + + @Override + public Long deserialize(JsonParser parser, DeserializationContext context) { + if (parser.hasToken(JsonToken.VALUE_NUMBER_FLOAT)) { + return context.reportInputMismatch(Long.class, MESG_PRIORITY_MUST_BE_INTEGER); + } + return _parseLong(parser, context, Long.class); + } + } } diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/function/management/FunctionWithLlmInvocationConfigPriorityTest.java b/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/function/management/FunctionWithLlmInvocationConfigPriorityTest.java index 113cbdd08..dd38d2aa8 100644 --- a/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/function/management/FunctionWithLlmInvocationConfigPriorityTest.java +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/function/management/FunctionWithLlmInvocationConfigPriorityTest.java @@ -386,6 +386,85 @@ void createWithDefaultPriorityAboveMaxIsRejected() { assertThat(response.getBody()).contains("defaultPriority"); } + @Test + void updateWithFractionalDefaultPriorityIsRejectedWithoutChangingPriority() { + var created = createLlmFunction(uniqueName("fractional-default"), priorityConfig(7L, null)); + var updateToken = MOCK_OAUTH2_TOKEN_SERVER.getJwt(TEST_CLIENT_SUBJECT, + List.of(SCOPE_UPDATE_FUNCTION), 100); + var updateEntity = RequestEntity.put(URI.create("/v2/nvcf/functions/" + created.id() + + "/versions/" + created.versionId())) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + updateToken) + .body(""" + { + "llmInvocationConfig": { + "priority": { + "defaultPriority": 1.5 + } + } + } + """); + + var updateResponse = testRestTemplate.exchange(updateEntity, String.class); + + assertThat(updateResponse.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + + var getToken = MOCK_OAUTH2_TOKEN_SERVER.getJwt(TEST_CLIENT_SUBJECT, + List.of(SCOPE_LIST_FUNCTIONS), 100); + var getEntity = RequestEntity.get(URI.create("/v2/nvcf/functions/" + created.id() + + "/versions/" + created.versionId())) + .header("Authorization", "Bearer " + getToken) + .build(); + var getResponse = testRestTemplate.exchange(getEntity, FunctionResponse.class); + + assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(getResponse.getBody()).isNotNull(); + assertThat(getResponse.getBody().function().llmInvocationConfig().priority().defaultPriority()) + .isEqualTo(7L); + } + + @Test + void updateWithFractionalPerAccountPriorityIsRejectedWithoutChangingPriority() { + var created = createLlmFunction( + uniqueName("fractional-per-account"), + priorityConfig(7L, Map.of(OVERRIDE_NCA_ID, 3L))); + var updateToken = MOCK_OAUTH2_TOKEN_SERVER.getJwt(TEST_CLIENT_SUBJECT, + List.of(SCOPE_UPDATE_FUNCTION), 100); + var updateEntity = RequestEntity.put(URI.create("/v2/nvcf/functions/" + created.id() + + "/versions/" + created.versionId())) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + updateToken) + .body(""" + { + "llmInvocationConfig": { + "priority": { + "defaultPriority": 7, + "perAccountPriority": { + "nca-override": 1.5 + } + } + } + } + """); + + var updateResponse = testRestTemplate.exchange(updateEntity, String.class); + + assertThat(updateResponse.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + + var getToken = MOCK_OAUTH2_TOKEN_SERVER.getJwt(TEST_CLIENT_SUBJECT, + List.of(SCOPE_LIST_FUNCTIONS), 100); + var getEntity = RequestEntity.get(URI.create("/v2/nvcf/functions/" + created.id() + + "/versions/" + created.versionId())) + .header("Authorization", "Bearer " + getToken) + .build(); + var getResponse = testRestTemplate.exchange(getEntity, FunctionResponse.class); + + assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(getResponse.getBody()).isNotNull(); + assertThat(getResponse.getBody().function().llmInvocationConfig().priority()) + .isEqualTo(new PriorityDto(7L, Map.of(OVERRIDE_NCA_ID, 3L))); + } + @Test void updateWithModelUpdatesAndLlmInvocationConfigAppliesBothToAllVersions() { var name = uniqueName("update-both");