From a66df30eee9387b52b9516c2b0acda3fee2466af Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Apr 2026 10:53:25 -0700 Subject: [PATCH 01/18] Implement #2459: allow overrides for "findAndRerank" command --- .../command/impl/FindAndRerankCommand.java | 48 +++++++++- .../jsonapi/exception/RequestException.java | 1 + .../FindAndRerankOperationBuilder.java | 93 ++++++++++++++++++- src/main/resources/errors.yaml | 10 ++ 4 files changed, 146 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java index 079ee9ae84..087ed1287a 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java @@ -2,6 +2,8 @@ import static io.stargate.sgv2.jsonapi.config.constants.DocumentConstants.Fields.*; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.core.JsonParser; @@ -16,6 +18,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterDefinition; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.FindAndRerankSort; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; +import io.stargate.sgv2.jsonapi.config.constants.ServiceDescConstants; import io.stargate.sgv2.jsonapi.metrics.CommandFeature; import io.stargate.sgv2.jsonapi.metrics.CommandFeatures; import io.stargate.sgv2.jsonapi.util.JsonFieldMatcher; @@ -24,6 +27,7 @@ import jakarta.validation.constraints.Positive; import java.io.IOException; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -103,7 +107,49 @@ public record Options( @Schema( description = "Return vector embedding used for ANN sorting.", type = SchemaType.BOOLEAN) - boolean includeSortVector) {} + boolean includeSortVector, + /** ---- */ + @Nullable + @Valid + @Schema( + description = + "Optional per-request override for the reranking service. Overrides the collection-level reranking provider, model, and/or authentication.", + implementation = RerankServiceOverride.class) + @JsonProperty("rerank") + RerankServiceOverride rerankServiceOverride) {} + + /** + * Optional per-request override for the reranking service configuration. All fields are optional; + * unspecified fields fall back to the collection's defaults. + */ + public record RerankServiceOverride( + @Nullable + @Schema( + description = "Override reranking service provider.", + type = SchemaType.STRING, + implementation = String.class) + @JsonProperty(ServiceDescConstants.PROVIDER) + String provider, + @Nullable + @Schema( + description = "Override reranking service model.", + type = SchemaType.STRING, + implementation = String.class) + @JsonProperty(ServiceDescConstants.MODEL_NAME) + String modelName, + @Nullable + @Schema( + description = "Override authentication config for reranking service.", + type = SchemaType.OBJECT) + @JsonProperty(ServiceDescConstants.AUTHENTICATION) + @JsonInclude(JsonInclude.Include.NON_NULL) + Map authentication) { + + @JsonIgnore + public boolean isEmpty() { + return provider == null && modelName == null && authentication == null; + } + } @JsonDeserialize(using = HybridLimitsDeserializer.class) public record HybridLimits( diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java index 9abb74b74a..d5b8fc9266 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java @@ -54,6 +54,7 @@ public enum Code implements ErrorCode { HYBRID_FIELD_UNSUPPORTED_SUBFIELD_VALUE_TYPE, INVALID_CREATE_COLLECTION_FIELD, + INVALID_RERANK_OVERRIDE, MISSING_RERANK_QUERY_TEXT, REQUEST_NOT_JSON, diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 3fe5a92921..7c953cffb2 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -24,7 +24,9 @@ import io.stargate.sgv2.jsonapi.service.operation.reranking.*; import io.stargate.sgv2.jsonapi.service.operation.tasks.*; import io.stargate.sgv2.jsonapi.service.provider.ApiModelSupport; +import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfig; import io.stargate.sgv2.jsonapi.service.reranking.operation.RerankingProvider; +import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionRerankDef; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; import io.stargate.sgv2.jsonapi.service.shredding.Deferrable; import io.stargate.sgv2.jsonapi.service.shredding.DeferredAction; @@ -56,6 +58,10 @@ class FindAndRerankOperationBuilder { private FindAndRerankCommand command; private FindCommandResolver findCommandResolver; + // lazily computed effective rerank service def (collection default merged with per-request + // override) + private CollectionRerankDef.RerankServiceDef effectiveRerankServiceDef; + public FindAndRerankOperationBuilder(CommandContext commandContext) { this.commandContext = Objects.requireNonNull(commandContext, "commandContext cannot be null"); @@ -164,11 +170,13 @@ private void checkSupported() { // TODO: more info in the error throw RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.get(); } + + // Resolve effective provider/model (collection default merged with any per-request override) + var effectiveServiceDef = resolveEffectiveRerankServiceDef(); + // Read is not supported for rerank model at END_OF_LIFE support status. var rerankingProvidersConfig = commandContext.rerankingProviderFactory().getRerankingConfig(); - var modelConfig = - rerankingProvidersConfig.filterByRerankServiceDef( - commandContext.schemaObject().rerankingConfig().rerankServiceDef()); + var modelConfig = rerankingProvidersConfig.filterByRerankServiceDef(effectiveServiceDef); // Validate if the model is END_OF_LIFE if (modelConfig.apiModelSupport().status() == ApiModelSupport.SupportStatus.END_OF_LIFE) { throw SchemaException.Code.END_OF_LIFE_AI_MODEL.get( @@ -188,8 +196,9 @@ private void checkSupported() { private TaskGroupAndDeferrables, CollectionSchemaObject> rerankTasks(List deferredCommandResults) { - // Previous code will check reranking is supported - var providerConfig = commandContext.schemaObject().rerankingConfig().rerankServiceDef(); + // Previous code will check reranking is supported; use the effective service def which + // may include per-request overrides + var providerConfig = resolveEffectiveRerankServiceDef(); RerankingProvider rerankingProvider = commandContext .rerankingProviderFactory() @@ -238,6 +247,80 @@ private void checkSupported() { .collect(java.util.stream.Collectors.toUnmodifiableList())); } + /** + * Resolves the effective RerankServiceDef by merging any per-request override from the command + * options with the collection's configured defaults. Result is memoized for the lifetime of this + * builder. + */ + private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef() { + if (effectiveRerankServiceDef != null) { + return effectiveRerankServiceDef; + } + + var collectionDef = commandContext.schemaObject().rerankingConfig().rerankServiceDef(); + var override = + getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); + + if (override == null || override.isEmpty()) { + effectiveRerankServiceDef = collectionDef; + return effectiveRerankServiceDef; + } + + // If provider is specified without modelName, error: we don't know which model + // the user wants from the new provider + if (override.provider() != null && override.modelName() == null) { + throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( + "message", "When overriding the reranking provider, 'modelName' must also be specified."); + } + + // Merge: override fields take precedence, nulls fall back to collection defaults + String effectiveProvider = + override.provider() != null ? override.provider() : collectionDef.provider(); + String effectiveModelName = + override.modelName() != null ? override.modelName() : collectionDef.modelName(); + Map effectiveAuth = + override.authentication() != null + ? override.authentication() + : collectionDef.authentication(); + + // Validate the effective provider+model against the provider registry + validateRerankOverride(effectiveProvider, effectiveModelName); + + effectiveRerankServiceDef = + new CollectionRerankDef.RerankServiceDef( + effectiveProvider, effectiveModelName, effectiveAuth, collectionDef.parameters()); + return effectiveRerankServiceDef; + } + + /** + * Validates that the overridden provider and model exist and are usable in the reranking + * providers configuration. + */ + private void validateRerankOverride(String provider, String modelName) { + var rerankingProvidersConfig = commandContext.rerankingProviderFactory().getRerankingConfig(); + + var providerConfig = rerankingProvidersConfig.providers().get(provider); + if (providerConfig == null) { + throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( + "message", "Reranking provider '%s' is not supported.".formatted(provider)); + } + if (!providerConfig.enabled()) { + throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( + "message", "Reranking provider '%s' is disabled.".formatted(provider)); + } + + RerankingProvidersConfig.RerankingProviderConfig.ModelConfig modelConfig = + providerConfig.models().stream() + .filter(m -> m.name().equals(modelName)) + .findFirst() + .orElse(null); + if (modelConfig == null) { + throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( + "message", + "Model '%s' is not supported by reranking provider '%s'.".formatted(modelName, provider)); + } + } + private TaskGroupAndDeferrables readTasks( RerankingTask.DeferredCommandWithSource deferredVectorRead, RerankingTask.DeferredCommandWithSource deferredBM25Read) { diff --git a/src/main/resources/errors.yaml b/src/main/resources/errors.yaml index 6d2df41abe..fdc92dd8d9 100644 --- a/src/main/resources/errors.yaml +++ b/src/main/resources/errors.yaml @@ -271,6 +271,16 @@ request-errors: Resend using a supported collection. + - scope: + code: INVALID_RERANK_OVERRIDE + title: Invalid reranking service override + body: |- + The per-request reranking service override in the findAndRerank command is invalid. + + ${message} + + Resend the command with a valid reranking service override, or omit the override to use the collection's default reranking configuration. + # unscoped because this touches both the sort and the options - scope: code: MISSING_RERANK_QUERY_TEXT From 55216df384602b787e911f991c4fb67995ab7384 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Apr 2026 10:59:48 -0700 Subject: [PATCH 02/18] Add block of DEPRECATED model overrides --- .../FindAndRerankOperationBuilder.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 7c953cffb2..152737c9b3 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -24,7 +24,6 @@ import io.stargate.sgv2.jsonapi.service.operation.reranking.*; import io.stargate.sgv2.jsonapi.service.operation.tasks.*; import io.stargate.sgv2.jsonapi.service.provider.ApiModelSupport; -import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfig; import io.stargate.sgv2.jsonapi.service.reranking.operation.RerankingProvider; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionRerankDef; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; @@ -309,7 +308,7 @@ private void validateRerankOverride(String provider, String modelName) { "message", "Reranking provider '%s' is disabled.".formatted(provider)); } - RerankingProvidersConfig.RerankingProviderConfig.ModelConfig modelConfig = + var modelConfig = providerConfig.models().stream() .filter(m -> m.name().equals(modelName)) .findFirst() @@ -319,6 +318,28 @@ private void validateRerankOverride(String provider, String modelName) { "message", "Model '%s' is not supported by reranking provider '%s'.".formatted(modelName, provider)); } + + // Block DEPRECATED and END_OF_LIFE models for per-request overrides (user is actively + // choosing this model, so both statuses should be rejected) + if (modelConfig.apiModelSupport().status() != ApiModelSupport.SupportStatus.SUPPORTED) { + var errorCode = + modelConfig.apiModelSupport().status() == ApiModelSupport.SupportStatus.DEPRECATED + ? SchemaException.Code.DEPRECATED_AI_MODEL + : SchemaException.Code.END_OF_LIFE_AI_MODEL; + throw errorCode.get( + Map.of( + "model", + modelConfig.name(), + "modelStatus", + modelConfig.apiModelSupport().status().name(), + "message", + modelConfig + .apiModelSupport() + .message() + .orElse( + "The model is %s." + .formatted(modelConfig.apiModelSupport().status().name())))); + } } private TaskGroupAndDeferrables readTasks( From 3df34e8eb22ecdfc4094dd376196857c0c566571 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Apr 2026 11:03:44 -0700 Subject: [PATCH 03/18] Config passing clean up --- .../FindAndRerankOperationBuilder.java | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 152737c9b3..7f6740b942 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -24,6 +24,7 @@ import io.stargate.sgv2.jsonapi.service.operation.reranking.*; import io.stargate.sgv2.jsonapi.service.operation.tasks.*; import io.stargate.sgv2.jsonapi.service.provider.ApiModelSupport; +import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfig; import io.stargate.sgv2.jsonapi.service.reranking.operation.RerankingProvider; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionRerankDef; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; @@ -170,34 +171,45 @@ private void checkSupported() { throw RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.get(); } - // Resolve effective provider/model (collection default merged with any per-request override) - var effectiveServiceDef = resolveEffectiveRerankServiceDef(); - - // Read is not supported for rerank model at END_OF_LIFE support status. + // Resolve effective provider/model (collection default merged with any per-request override). + // For overrides, validateRerankOverride() already checks DEPRECATED and END_OF_LIFE. + // For collection defaults (no override), we still need the END_OF_LIFE check below since + // the model may have become EOL after the collection was created. var rerankingProvidersConfig = commandContext.rerankingProviderFactory().getRerankingConfig(); - var modelConfig = rerankingProvidersConfig.filterByRerankServiceDef(effectiveServiceDef); - // Validate if the model is END_OF_LIFE - if (modelConfig.apiModelSupport().status() == ApiModelSupport.SupportStatus.END_OF_LIFE) { - throw SchemaException.Code.END_OF_LIFE_AI_MODEL.get( - Map.of( - "model", - modelConfig.name(), - "modelStatus", - modelConfig.apiModelSupport().status().name(), - "message", - modelConfig - .apiModelSupport() - .message() - .orElse("The model is no longer supported (reached its end-of-life)."))); + var effectiveServiceDef = resolveEffectiveRerankServiceDef(rerankingProvidersConfig); + + if (!hasRerankOverride()) { + var modelConfig = rerankingProvidersConfig.filterByRerankServiceDef(effectiveServiceDef); + if (modelConfig.apiModelSupport().status() == ApiModelSupport.SupportStatus.END_OF_LIFE) { + throw SchemaException.Code.END_OF_LIFE_AI_MODEL.get( + Map.of( + "model", + modelConfig.name(), + "modelStatus", + modelConfig.apiModelSupport().status().name(), + "message", + modelConfig + .apiModelSupport() + .message() + .orElse("The model is no longer supported (reached its end-of-life)."))); + } } } + private boolean hasRerankOverride() { + var override = + getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); + return override != null && !override.isEmpty(); + } + private TaskGroupAndDeferrables, CollectionSchemaObject> rerankTasks(List deferredCommandResults) { // Previous code will check reranking is supported; use the effective service def which // may include per-request overrides - var providerConfig = resolveEffectiveRerankServiceDef(); + var providerConfig = + resolveEffectiveRerankServiceDef( + commandContext.rerankingProviderFactory().getRerankingConfig()); RerankingProvider rerankingProvider = commandContext .rerankingProviderFactory() @@ -251,7 +263,8 @@ private void checkSupported() { * options with the collection's configured defaults. Result is memoized for the lifetime of this * builder. */ - private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef() { + private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef( + RerankingProvidersConfig rerankingProvidersConfig) { if (effectiveRerankServiceDef != null) { return effectiveRerankServiceDef; } @@ -283,7 +296,7 @@ private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef() : collectionDef.authentication(); // Validate the effective provider+model against the provider registry - validateRerankOverride(effectiveProvider, effectiveModelName); + validateRerankOverride(rerankingProvidersConfig, effectiveProvider, effectiveModelName); effectiveRerankServiceDef = new CollectionRerankDef.RerankServiceDef( @@ -295,9 +308,8 @@ private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef() * Validates that the overridden provider and model exist and are usable in the reranking * providers configuration. */ - private void validateRerankOverride(String provider, String modelName) { - var rerankingProvidersConfig = commandContext.rerankingProviderFactory().getRerankingConfig(); - + private void validateRerankOverride( + RerankingProvidersConfig rerankingProvidersConfig, String provider, String modelName) { var providerConfig = rerankingProvidersConfig.providers().get(provider); if (providerConfig == null) { throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( From 5d43561d7add449255fe26e52243744947ba3250 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Apr 2026 11:06:16 -0700 Subject: [PATCH 04/18] Change to coerce blank Strings to nulls for options --- .../api/model/command/impl/FindAndRerankCommand.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java index 087ed1287a..9e3abb3baa 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java @@ -145,10 +145,20 @@ public record RerankServiceOverride( @JsonInclude(JsonInclude.Include.NON_NULL) Map authentication) { + /** Normalize blank strings to null so downstream code can use simple null checks. */ + public RerankServiceOverride { + provider = blankToNull(provider); + modelName = blankToNull(modelName); + } + @JsonIgnore public boolean isEmpty() { return provider == null && modelName == null && authentication == null; } + + private static String blankToNull(String value) { + return (value != null && value.isBlank()) ? null : value; + } } @JsonDeserialize(using = HybridLimitsDeserializer.class) From f2bdfa30fc26ed066bd992fc6b677c3e7fbf030b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Apr 2026 11:09:51 -0700 Subject: [PATCH 05/18] Extract helper method --- .../api/model/command/impl/FindAndRerankCommand.java | 9 +++------ .../java/io/stargate/sgv2/jsonapi/util/StringUtil.java | 8 ++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java index 9e3abb3baa..64c614ec99 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java @@ -22,6 +22,7 @@ import io.stargate.sgv2.jsonapi.metrics.CommandFeature; import io.stargate.sgv2.jsonapi.metrics.CommandFeatures; import io.stargate.sgv2.jsonapi.util.JsonFieldMatcher; +import io.stargate.sgv2.jsonapi.util.StringUtil; import io.stargate.sgv2.jsonapi.util.recordable.Recordable; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; @@ -147,18 +148,14 @@ public record RerankServiceOverride( /** Normalize blank strings to null so downstream code can use simple null checks. */ public RerankServiceOverride { - provider = blankToNull(provider); - modelName = blankToNull(modelName); + provider = StringUtil.blankToNull(provider); + modelName = StringUtil.blankToNull(modelName); } @JsonIgnore public boolean isEmpty() { return provider == null && modelName == null && authentication == null; } - - private static String blankToNull(String value) { - return (value != null && value.isBlank()) ? null : value; - } } @JsonDeserialize(using = HybridLimitsDeserializer.class) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java b/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java index 9e34040f8d..9bd6ddb96f 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java @@ -13,4 +13,12 @@ public static String normalizeOptionalString(String string) { public static String normalizeOptionalString(Optional string) { return normalizeOptionalString(string.orElse("")); } + + /** + * Returns null if the string is null, empty, or contains only whitespace; otherwise String + * itself. + */ + public static String blankToNull(String value) { + return (value != null && value.isBlank()) ? null : value; + } } From fd060f982aa9aa856929ca59e528c14c1ee7f5b9 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Apr 2026 11:36:32 -0700 Subject: [PATCH 06/18] Further correctness fixes --- .../model/command/impl/FindAndRerankCommand.java | 4 +++- .../resolver/FindAndRerankOperationBuilder.java | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java index 64c614ec99..e64f2e479c 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java @@ -154,7 +154,9 @@ public record RerankServiceOverride( @JsonIgnore public boolean isEmpty() { - return provider == null && modelName == null && authentication == null; + return provider == null + && modelName == null + && (authentication == null || authentication.isEmpty()); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 7f6740b942..bad3a3a8ff 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -196,10 +196,15 @@ private void checkSupported() { } } + /** + * Returns true when the per-request override specifies a different provider or model. Auth-only + * overrides return false because the collection's own provider/model is unchanged and should go + * through the normal (EOL-only) validation path rather than the stricter override validation. + */ private boolean hasRerankOverride() { var override = getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); - return override != null && !override.isEmpty(); + return override != null && (override.provider() != null || override.modelName() != null); } private TaskGroupAndDeferrables, CollectionSchemaObject> @@ -295,8 +300,13 @@ private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef( ? override.authentication() : collectionDef.authentication(); - // Validate the effective provider+model against the provider registry - validateRerankOverride(rerankingProvidersConfig, effectiveProvider, effectiveModelName); + // Validate the effective provider+model against the provider registry, but only when + // the provider or model is actually being overridden. Auth-only overrides reuse the + // collection's provider/model unchanged, so they go through the normal EOL-only check + // in checkSupported() instead of the stricter DEPRECATED+EOL check here. + if (override.provider() != null || override.modelName() != null) { + validateRerankOverride(rerankingProvidersConfig, effectiveProvider, effectiveModelName); + } effectiveRerankServiceDef = new CollectionRerankDef.RerankServiceDef( From 51e58a9bae5cd859b4fe2fc3a920dd0685d68840 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Apr 2026 12:41:04 -0700 Subject: [PATCH 07/18] Unify empty/missing value handling --- .../api/model/command/impl/FindAndRerankCommand.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java index e64f2e479c..27f2a621c1 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java @@ -109,7 +109,6 @@ public record Options( description = "Return vector embedding used for ANN sorting.", type = SchemaType.BOOLEAN) boolean includeSortVector, - /** ---- */ @Nullable @Valid @Schema( @@ -150,13 +149,12 @@ public record RerankServiceOverride( public RerankServiceOverride { provider = StringUtil.blankToNull(provider); modelName = StringUtil.blankToNull(modelName); + authentication = (authentication == null || authentication.isEmpty()) ? null : authentication; } @JsonIgnore public boolean isEmpty() { - return provider == null - && modelName == null - && (authentication == null || authentication.isEmpty()); + return provider == null && modelName == null && authentication == null; } } From 361c726fe863cc2e2b6a2ec3e43162fc2670b8bb Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 10:05:12 -0700 Subject: [PATCH 08/18] Rework based on clarified definition (update issue description). --- .../command/impl/FindAndRerankCommand.java | 53 ++----------- .../FindAndRerankOperationBuilder.java | 74 ++++++++----------- src/main/resources/errors.yaml | 10 +-- 3 files changed, 39 insertions(+), 98 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java index 27f2a621c1..c68af8a475 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindAndRerankCommand.java @@ -2,8 +2,6 @@ import static io.stargate.sgv2.jsonapi.config.constants.DocumentConstants.Fields.*; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.core.JsonParser; @@ -18,17 +16,14 @@ import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterDefinition; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.FindAndRerankSort; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; -import io.stargate.sgv2.jsonapi.config.constants.ServiceDescConstants; import io.stargate.sgv2.jsonapi.metrics.CommandFeature; import io.stargate.sgv2.jsonapi.metrics.CommandFeatures; import io.stargate.sgv2.jsonapi.util.JsonFieldMatcher; -import io.stargate.sgv2.jsonapi.util.StringUtil; import io.stargate.sgv2.jsonapi.util.recordable.Recordable; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import java.io.IOException; import java.util.List; -import java.util.Map; import javax.annotation.Nullable; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -113,50 +108,12 @@ public record Options( @Valid @Schema( description = - "Optional per-request override for the reranking service. Overrides the collection-level reranking provider, model, and/or authentication.", - implementation = RerankServiceOverride.class) + "Optional per-request override for the reranking service. Completely replaces the" + + " collection-level reranking configuration when provided. Both provider and" + + " modelName are required.", + implementation = CreateCollectionCommand.Options.RerankServiceDesc.class) @JsonProperty("rerank") - RerankServiceOverride rerankServiceOverride) {} - - /** - * Optional per-request override for the reranking service configuration. All fields are optional; - * unspecified fields fall back to the collection's defaults. - */ - public record RerankServiceOverride( - @Nullable - @Schema( - description = "Override reranking service provider.", - type = SchemaType.STRING, - implementation = String.class) - @JsonProperty(ServiceDescConstants.PROVIDER) - String provider, - @Nullable - @Schema( - description = "Override reranking service model.", - type = SchemaType.STRING, - implementation = String.class) - @JsonProperty(ServiceDescConstants.MODEL_NAME) - String modelName, - @Nullable - @Schema( - description = "Override authentication config for reranking service.", - type = SchemaType.OBJECT) - @JsonProperty(ServiceDescConstants.AUTHENTICATION) - @JsonInclude(JsonInclude.Include.NON_NULL) - Map authentication) { - - /** Normalize blank strings to null so downstream code can use simple null checks. */ - public RerankServiceOverride { - provider = StringUtil.blankToNull(provider); - modelName = StringUtil.blankToNull(modelName); - authentication = (authentication == null || authentication.isEmpty()) ? null : authentication; - } - - @JsonIgnore - public boolean isEmpty() { - return provider == null && modelName == null && authentication == null; - } - } + CreateCollectionCommand.Options.RerankServiceDesc rerankServiceOverride) {} @JsonDeserialize(using = HybridLimitsDeserializer.class) public record HybridLimits( diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index bad3a3a8ff..74994a9272 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -58,8 +58,7 @@ class FindAndRerankOperationBuilder { private FindAndRerankCommand command; private FindCommandResolver findCommandResolver; - // lazily computed effective rerank service def (collection default merged with per-request - // override) + // lazily computed effective rerank service def (per-request override or collection default) private CollectionRerankDef.RerankServiceDef effectiveRerankServiceDef; public FindAndRerankOperationBuilder(CommandContext commandContext) { @@ -166,8 +165,7 @@ private void checkSupported() { } } - if (!commandContext.schemaObject().rerankingConfig().enabled()) { - // TODO: more info in the error + if (!commandContext.schemaObject().rerankingConfig().enabled() && !hasRerankOverride()) { throw RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.get(); } @@ -196,15 +194,11 @@ private void checkSupported() { } } - /** - * Returns true when the per-request override specifies a different provider or model. Auth-only - * overrides return false because the collection's own provider/model is unchanged and should go - * through the normal (EOL-only) validation path rather than the stricter override validation. - */ + /** Returns true when a per-request reranking service override is present. */ private boolean hasRerankOverride() { var override = getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); - return override != null && (override.provider() != null || override.modelName() != null); + return override != null && !override.isEmpty(); } private TaskGroupAndDeferrables, CollectionSchemaObject> @@ -264,9 +258,9 @@ private boolean hasRerankOverride() { } /** - * Resolves the effective RerankServiceDef by merging any per-request override from the command - * options with the collection's configured defaults. Result is memoized for the lifetime of this - * builder. + * Resolves the effective RerankServiceDef: uses the per-request override if present (completely + * replacing collection config), otherwise falls back to the collection's configured defaults. + * Result is memoized for the lifetime of this builder. */ private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef( RerankingProvidersConfig rerankingProvidersConfig) { @@ -274,43 +268,25 @@ private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef( return effectiveRerankServiceDef; } - var collectionDef = commandContext.schemaObject().rerankingConfig().rerankServiceDef(); var override = getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); - if (override == null || override.isEmpty()) { - effectiveRerankServiceDef = collectionDef; - return effectiveRerankServiceDef; - } - - // If provider is specified without modelName, error: we don't know which model - // the user wants from the new provider - if (override.provider() != null && override.modelName() == null) { - throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( - "message", "When overriding the reranking provider, 'modelName' must also be specified."); - } + if (override != null && !override.isEmpty()) { + // Per-request override: use it entirely, no merge with collection config. + validateRerankOverride(rerankingProvidersConfig, override.provider(), override.modelName()); - // Merge: override fields take precedence, nulls fall back to collection defaults - String effectiveProvider = - override.provider() != null ? override.provider() : collectionDef.provider(); - String effectiveModelName = - override.modelName() != null ? override.modelName() : collectionDef.modelName(); - Map effectiveAuth = - override.authentication() != null - ? override.authentication() - : collectionDef.authentication(); - - // Validate the effective provider+model against the provider registry, but only when - // the provider or model is actually being overridden. Auth-only overrides reuse the - // collection's provider/model unchanged, so they go through the normal EOL-only check - // in checkSupported() instead of the stricter DEPRECATED+EOL check here. - if (override.provider() != null || override.modelName() != null) { - validateRerankOverride(rerankingProvidersConfig, effectiveProvider, effectiveModelName); + effectiveRerankServiceDef = + new CollectionRerankDef.RerankServiceDef( + override.provider(), + override.modelName(), + override.authentication(), + override.parameters()); + } else { + // No override: use collection config (guaranteed non-null here because checkSupported() + // already blocked the disabled-collection-without-override case). + effectiveRerankServiceDef = + commandContext.schemaObject().rerankingConfig().rerankServiceDef(); } - - effectiveRerankServiceDef = - new CollectionRerankDef.RerankServiceDef( - effectiveProvider, effectiveModelName, effectiveAuth, collectionDef.parameters()); return effectiveRerankServiceDef; } @@ -320,6 +296,14 @@ private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef( */ private void validateRerankOverride( RerankingProvidersConfig rerankingProvidersConfig, String provider, String modelName) { + // provider is guaranteed non-null by @NotNull on RerankServiceDesc.provider; + // modelName has no @NotNull so we must check explicitly + if (modelName == null) { + throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( + "message", + "The 'modelName' field is required when specifying a reranking service override."); + } + var providerConfig = rerankingProvidersConfig.providers().get(provider); if (providerConfig == null) { throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( diff --git a/src/main/resources/errors.yaml b/src/main/resources/errors.yaml index fdc92dd8d9..500bf50cfb 100644 --- a/src/main/resources/errors.yaml +++ b/src/main/resources/errors.yaml @@ -265,11 +265,11 @@ request-errors: code: UNSUPPORTED_RERANKING_COMMAND title: Reranking not enabled for collection body: |- - The `findAndRerank` is not enabled for the collection. - - The Command is only supported for collections that have `vector`, `lexical`, and `rerank` configured. They additionally require a vectorize configuration if `$vectorize` is used. - - Resend using a supported collection. + The `findAndRerank` is not enabled for the collection and no per-request reranking service override was provided. + + The Command is only supported for collections that have `vector`, `lexical`, and `rerank` configured, or when a per-request reranking service override is provided via the `rerank` option. They additionally require a vectorize configuration if `$vectorize` is used. + + Resend using a supported collection or provide a reranking service override in the command options. - scope: code: INVALID_RERANK_OVERRIDE From 1f72f780e48a3edb22081220b8afcb3418a7c7cd Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 10:07:37 -0700 Subject: [PATCH 09/18] Minor comment improvement --- .../jsonapi/service/resolver/FindAndRerankOperationBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 74994a9272..1a705f9084 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -169,7 +169,7 @@ private void checkSupported() { throw RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.get(); } - // Resolve effective provider/model (collection default merged with any per-request override). + // Resolve effective provider/model (per-request override replaces collection config entirely). // For overrides, validateRerankOverride() already checks DEPRECATED and END_OF_LIFE. // For collection defaults (no override), we still need the END_OF_LIFE check below since // the model may have become EOL after the collection was created. From 2dcff8c71d1fb2ea79cd583bc4c9538f5fc6bd49 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 10:23:29 -0700 Subject: [PATCH 10/18] Streamlining --- .../FindAndRerankOperationBuilder.java | 87 +++++++------------ 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 1a705f9084..08cedef6aa 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -165,19 +165,34 @@ private void checkSupported() { } } - if (!commandContext.schemaObject().rerankingConfig().enabled() && !hasRerankOverride()) { + var rerankOverride = + getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); + boolean hasOverride = rerankOverride != null && !rerankOverride.isEmpty(); + + if (!commandContext.schemaObject().rerankingConfig().enabled() && !hasOverride) { throw RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.get(); } - // Resolve effective provider/model (per-request override replaces collection config entirely). - // For overrides, validateRerankOverride() already checks DEPRECATED and END_OF_LIFE. - // For collection defaults (no override), we still need the END_OF_LIFE check below since - // the model may have become EOL after the collection was created. - var rerankingProvidersConfig = commandContext.rerankingProviderFactory().getRerankingConfig(); - var effectiveServiceDef = resolveEffectiveRerankServiceDef(rerankingProvidersConfig); - - if (!hasRerankOverride()) { - var modelConfig = rerankingProvidersConfig.filterByRerankServiceDef(effectiveServiceDef); + // Resolve effective rerank service def: per-request override replaces collection config + // entirely; otherwise fall back to the collection's configured defaults. + if (hasOverride) { + var rerankingProvidersConfig = commandContext.rerankingProviderFactory().getRerankingConfig(); + validateRerankOverride( + rerankingProvidersConfig, rerankOverride.provider(), rerankOverride.modelName()); + effectiveRerankServiceDef = + new CollectionRerankDef.RerankServiceDef( + rerankOverride.provider(), + rerankOverride.modelName(), + rerankOverride.authentication(), + rerankOverride.parameters()); + } else { + // Collection defaults: check END_OF_LIFE since model may have become EOL after creation. + // (validateRerankOverride already covers DEPRECATED+EOL for overrides above.) + effectiveRerankServiceDef = + commandContext.schemaObject().rerankingConfig().rerankServiceDef(); + var rerankingProvidersConfig = commandContext.rerankingProviderFactory().getRerankingConfig(); + var modelConfig = + rerankingProvidersConfig.filterByRerankServiceDef(effectiveRerankServiceDef); if (modelConfig.apiModelSupport().status() == ApiModelSupport.SupportStatus.END_OF_LIFE) { throw SchemaException.Code.END_OF_LIFE_AI_MODEL.get( Map.of( @@ -194,30 +209,19 @@ private void checkSupported() { } } - /** Returns true when a per-request reranking service override is present. */ - private boolean hasRerankOverride() { - var override = - getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); - return override != null && !override.isEmpty(); - } - private TaskGroupAndDeferrables, CollectionSchemaObject> rerankTasks(List deferredCommandResults) { - // Previous code will check reranking is supported; use the effective service def which - // may include per-request overrides - var providerConfig = - resolveEffectiveRerankServiceDef( - commandContext.rerankingProviderFactory().getRerankingConfig()); + // checkSupported() already resolved effectiveRerankServiceDef RerankingProvider rerankingProvider = commandContext .rerankingProviderFactory() .create( commandContext.requestContext().tenant(), commandContext.requestContext().authToken(), - providerConfig.provider(), - providerConfig.modelName(), - providerConfig.authentication(), + effectiveRerankServiceDef.provider(), + effectiveRerankServiceDef.modelName(), + effectiveRerankServiceDef.authentication(), commandContext.commandName()); // todo: move to a builder pattern, mosty to make it easier to manage the task position and @@ -257,39 +261,6 @@ private boolean hasRerankOverride() { .collect(java.util.stream.Collectors.toUnmodifiableList())); } - /** - * Resolves the effective RerankServiceDef: uses the per-request override if present (completely - * replacing collection config), otherwise falls back to the collection's configured defaults. - * Result is memoized for the lifetime of this builder. - */ - private CollectionRerankDef.RerankServiceDef resolveEffectiveRerankServiceDef( - RerankingProvidersConfig rerankingProvidersConfig) { - if (effectiveRerankServiceDef != null) { - return effectiveRerankServiceDef; - } - - var override = - getOrDefault(command.options(), FindAndRerankCommand.Options::rerankServiceOverride, null); - - if (override != null && !override.isEmpty()) { - // Per-request override: use it entirely, no merge with collection config. - validateRerankOverride(rerankingProvidersConfig, override.provider(), override.modelName()); - - effectiveRerankServiceDef = - new CollectionRerankDef.RerankServiceDef( - override.provider(), - override.modelName(), - override.authentication(), - override.parameters()); - } else { - // No override: use collection config (guaranteed non-null here because checkSupported() - // already blocked the disabled-collection-without-override case). - effectiveRerankServiceDef = - commandContext.schemaObject().rerankingConfig().rerankServiceDef(); - } - return effectiveRerankServiceDef; - } - /** * Validates that the overridden provider and model exist and are usable in the reranking * providers configuration. From 930f0153b935087d7f0755c381504baad9f9d80a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 10:29:53 -0700 Subject: [PATCH 11/18] Minor reordering of validation --- .../resolver/FindAndRerankOperationBuilder.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 08cedef6aa..3765325f6c 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -267,14 +267,6 @@ private void checkSupported() { */ private void validateRerankOverride( RerankingProvidersConfig rerankingProvidersConfig, String provider, String modelName) { - // provider is guaranteed non-null by @NotNull on RerankServiceDesc.provider; - // modelName has no @NotNull so we must check explicitly - if (modelName == null) { - throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( - "message", - "The 'modelName' field is required when specifying a reranking service override."); - } - var providerConfig = rerankingProvidersConfig.providers().get(provider); if (providerConfig == null) { throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( @@ -284,7 +276,13 @@ private void validateRerankOverride( throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( "message", "Reranking provider '%s' is disabled.".formatted(provider)); } - + // provider is guaranteed non-null by @NotNull on RerankServiceDesc.provider; + // modelName has no @NotNull so we must check explicitly + if (modelName == null) { + throw RequestException.Code.INVALID_RERANK_OVERRIDE.get( + "message", + "The 'modelName' field is required when specifying a reranking service override."); + } var modelConfig = providerConfig.models().stream() .filter(m -> m.name().equals(modelName)) From 743fd15d3bd174f9bae6e2d277070f860a45d052 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 14:27:41 -0700 Subject: [PATCH 12/18] Add unit tests --- .../FindAndRerankOperationBuilder.java | 3 +- .../FindAndRerankOperationBuilderTest.java | 220 ++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 3765325f6c..7ed5393102 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -265,7 +265,8 @@ private void checkSupported() { * Validates that the overridden provider and model exist and are usable in the reranking * providers configuration. */ - private void validateRerankOverride( + // package-private for unit testing + void validateRerankOverride( RerankingProvidersConfig rerankingProvidersConfig, String provider, String modelName) { var providerConfig = rerankingProvidersConfig.providers().get(provider); if (providerConfig == null) { diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java new file mode 100644 index 0000000000..3776055afe --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java @@ -0,0 +1,220 @@ +package io.stargate.sgv2.jsonapi.service.resolver; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.stargate.sgv2.jsonapi.TestConstants; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.request.RequestContext; +import io.stargate.sgv2.jsonapi.exception.RequestException; +import io.stargate.sgv2.jsonapi.exception.SchemaException; +import io.stargate.sgv2.jsonapi.service.provider.ApiModelSupport; +import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfig; +import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfigImpl; +import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; +import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +class FindAndRerankOperationBuilderTest { + + @InjectMock protected RequestContext dataApiRequestInfo; + + private final TestConstants testConstants = new TestConstants(); + + private FindAndRerankOperationBuilder builder; + + // Reusable request properties for model configs + private static final RerankingProvidersConfigImpl.RerankingProviderConfigImpl.ModelConfigImpl + .RequestPropertiesImpl + REQUEST_PROPERTIES = + new RerankingProvidersConfigImpl.RerankingProviderConfigImpl.ModelConfigImpl + .RequestPropertiesImpl(3, 10, 100, 100, 0.5, 10); + + @BeforeEach + void beforeEach() { + CommandContext commandContext = testConstants.collectionContext(); + builder = new FindAndRerankOperationBuilder(commandContext); + } + + private static RerankingProvidersConfig.RerankingProviderConfig.ModelConfig modelConfig( + String name, ApiModelSupport.SupportStatus status) { + return new RerankingProvidersConfigImpl.RerankingProviderConfigImpl.ModelConfigImpl( + name, + new ApiModelSupport.ApiModelSupportImpl(status, Optional.empty()), + false, + "https://example.com/rerank", + REQUEST_PROPERTIES); + } + + private static RerankingProvidersConfig configWithProvider( + String providerName, + boolean enabled, + List models) { + return new RerankingProvidersConfigImpl( + Map.of( + providerName, + new RerankingProvidersConfigImpl.RerankingProviderConfigImpl( + false, providerName, enabled, Map.of(), models))); + } + + @Nested + class ValidateRerankOverride { + + @Test + void shouldAcceptSupportedProviderAndModel() { + var config = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatCode(() -> builder.validateRerankOverride(config, "nvidia", "nvidia/rerank-v1")) + .doesNotThrowAnyException(); + } + + @Test + void shouldRejectUnknownProvider() { + var config = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatThrownBy( + () -> builder.validateRerankOverride(config, "unknown-provider", "some-model")) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) + .hasMessageContaining("unknown-provider"); + } + + @Test + void shouldRejectDisabledProvider() { + var config = + configWithProvider( + "nvidia", + false, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", "nvidia/rerank-v1")) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) + .hasMessageContaining("disabled"); + } + + @Test + void shouldRejectNullModelName() { + var config = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", null)) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) + .hasMessageContaining("modelName"); + } + + @Test + void shouldRejectUnknownModel() { + var config = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatThrownBy( + () -> builder.validateRerankOverride(config, "nvidia", "nvidia/nonexistent-model")) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) + .hasMessageContaining("nonexistent-model"); + } + + @Test + void shouldRejectDeprecatedModel() { + var config = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/old-model", ApiModelSupport.SupportStatus.DEPRECATED))); + + assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", "nvidia/old-model")) + .isInstanceOf(SchemaException.class) + .hasFieldOrPropertyWithValue("code", SchemaException.Code.DEPRECATED_AI_MODEL.name()); + } + + @Test + void shouldRejectEndOfLifeModel() { + var config = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/eol-model", ApiModelSupport.SupportStatus.END_OF_LIFE))); + + assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", "nvidia/eol-model")) + .isInstanceOf(SchemaException.class) + .hasFieldOrPropertyWithValue("code", SchemaException.Code.END_OF_LIFE_AI_MODEL.name()); + } + + @Test + void shouldRejectNullModelNameBeforeCheckingModelExistence() { + // Even when the provider has models, null modelName should be caught + // before the stream filter (which would NPE on .equals(null)) + var config = + configWithProvider( + "nvidia", + true, + List.of( + modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED), + modelConfig("nvidia/rerank-v2", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", null)) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue( + "code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()); + } + + @Test + void shouldRejectUnknownProviderBeforeCheckingModelName() { + // When both provider is unknown AND modelName is null, the provider check should + // come first — user gets the more actionable "provider not supported" error + var config = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatThrownBy(() -> builder.validateRerankOverride(config, "unknown-provider", null)) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) + .hasMessageContaining("unknown-provider") + .hasMessageContaining("not supported"); + } + + @Test + void shouldRejectDisabledProviderBeforeCheckingModelName() { + // When provider is disabled AND modelName is null, the disabled check should + // come first — user gets "provider disabled" instead of "modelName required" + var config = + configWithProvider( + "nvidia", + false, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + + assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", null)) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) + .hasMessageContaining("disabled"); + } + } +} From 5d705e9ac0dde3fc9d0967097342bd57489cb649 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 14:41:21 -0700 Subject: [PATCH 13/18] Add basic ITs as well --- ...indAndRerankCollectionIntegrationTest.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java index 49caa1eeee..c41d5e7fb2 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java @@ -7,6 +7,7 @@ import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.stargate.sgv2.jsonapi.exception.RequestException; +import io.stargate.sgv2.jsonapi.exception.SchemaException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; @@ -166,6 +167,123 @@ private void errorOnNotEnabled( containsString(errorMessageContains.formatted(keyspaceName, collectionName))); } + // ---- Per-request reranking override tests ---- + // These use a collection with vectorize enabled but NO reranking configured. + // Sort uses $hybrid with only $vectorize (no $lexical) to avoid needing lexical support. + + private static final String VECTORIZE_NO_RERANK_SPEC = + """ + { + "name" : "%s", + "options": { + "vector": { + "metric": "cosine", + "dimension": 1024, + "service": { + "provider": "openai", + "modelName": "text-embedding-3-small" + } + } + } + } + """; + + @Test + void failOnRerankingDisabledNoOverride() { + String collectionName = "rerank_disabled_no_override"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen(keyspaceName, collectionName, findAndRerankWithOverride(null)) + .body("$", responseIsError()) + .body("errors[0].errorCode", is(RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.name())) + .body("errors[0].message", containsString("per-request reranking service override")); + } + + @Test + void failOnRerankOverrideUnknownProvider() { + String collectionName = "rerank_override_bad_provider"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen( + keyspaceName, + collectionName, + findAndRerankWithOverride( + """ + {"provider": "unknown-provider", "modelName": "some-model"} + """)) + .body("$", responseIsError()) + .body("errors[0].errorCode", is(RequestException.Code.INVALID_RERANK_OVERRIDE.name())) + .body("errors[0].message", containsString("unknown-provider")); + } + + @Test + void failOnRerankOverrideMissingModelName() { + String collectionName = "rerank_override_no_model"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen( + keyspaceName, + collectionName, + findAndRerankWithOverride( + """ + {"provider": "nvidia"} + """)) + .body("$", responseIsError()) + .body("errors[0].errorCode", is(RequestException.Code.INVALID_RERANK_OVERRIDE.name())) + .body("errors[0].message", containsString("modelName")); + } + + @Test + void failOnRerankOverrideDeprecatedModel() { + String collectionName = "rerank_override_deprecated"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen( + keyspaceName, + collectionName, + findAndRerankWithOverride( + """ + {"provider": "nvidia", "modelName": "nvidia/a-random-deprecated-model"} + """)) + .body("$", responseIsError()) + .body("errors[0].errorCode", is(SchemaException.Code.DEPRECATED_AI_MODEL.name())); + } + + @Test + void failOnRerankOverrideEolModel() { + String collectionName = "rerank_override_eol"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen( + keyspaceName, + collectionName, + findAndRerankWithOverride( + """ + {"provider": "nvidia", "modelName": "nvidia/a-random-EOL-model"} + """)) + .body("$", responseIsError()) + .body("errors[0].errorCode", is(SchemaException.Code.END_OF_LIFE_AI_MODEL.name())); + } + + /** + * Builds a findAndRerank command JSON with an optional rerank override in options. + * + * @param rerankOverrideJson JSON object for the "rerank" option, or null for no override. + */ + private static String findAndRerankWithOverride(String rerankOverrideJson) { + String rerankOption = rerankOverrideJson != null ? ", \"rerank\": " + rerankOverrideJson : ""; + return + """ + {"findAndRerank": { + "sort": {"$hybrid": {"$vectorize": "search text"}}, + "options": { + "limit": 10%s + } + }} + """ + .formatted(rerankOption); + } + private void createCollectionWithCleanup(String collectionName, String collectionSpec) { createComplexCollection(collectionSpec.formatted(collectionName)); // save the collection name for cleanup, but only after successful creation From ddec95cb7bd769040bff3649a1cfd58250d6eda7 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 14:56:02 -0700 Subject: [PATCH 14/18] Moar testing --- ...indAndRerankCollectionIntegrationTest.java | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java index c41d5e7fb2..52de93c47d 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java @@ -1,8 +1,7 @@ package io.stargate.sgv2.jsonapi.api.v1; import static io.stargate.sgv2.jsonapi.api.v1.ResponseAssertions.responseIsError; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.*; import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; @@ -23,7 +22,7 @@ @WithTestResource(value = DseTestResource.class) public class FindAndRerankCollectionIntegrationTest extends AbstractCollectionIntegrationTestBase { - // used to cleanup the collection from a previous test, if non-null + // used to clean up the collection from a previous test, if non-null private String cleanupCollectionName = null; @BeforeAll @@ -265,6 +264,57 @@ void failOnRerankOverrideEolModel() { .body("errors[0].errorCode", is(SchemaException.Code.END_OF_LIFE_AI_MODEL.name())); } + @Test + void overrideWithAuthPassesValidation() { + // Override with valid provider+model and authentication credentials. + // Should pass all override validation and fail downstream (e.g., embedding provider), + // NOT with a rerank validation error. + String collectionName = "rerank_override_with_auth"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen( + keyspaceName, + collectionName, + findAndRerankWithOverride( + """ + {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", + "authentication": {"providerKey": "my-test-key"}} + """)) + .body("$", responseIsError()) + .body( + "errors[0].errorCode", + not( + anyOf( + is(RequestException.Code.INVALID_RERANK_OVERRIDE.name()), + is(RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.name())))); + } + + @Test + void overrideWithAuthAndParametersPassesValidation() { + // Override with valid provider+model, authentication, and parameters. + // Should pass all override validation and fail downstream, + // NOT with a rerank validation error. + String collectionName = "rerank_override_auth_params"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen( + keyspaceName, + collectionName, + findAndRerankWithOverride( + """ + {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", + "authentication": {"providerKey": "my-test-key"}, + "parameters": {"truncate": "END"}} + """)) + .body("$", responseIsError()) + .body( + "errors[0].errorCode", + not( + anyOf( + is(RequestException.Code.INVALID_RERANK_OVERRIDE.name()), + is(RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.name())))); + } + /** * Builds a findAndRerank command JSON with an optional rerank override in options. * From bcdfa4630fcf1c6a460706c7913671165ce837d1 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 15:07:53 -0700 Subject: [PATCH 15/18] Improve ITs --- ...indAndRerankCollectionIntegrationTest.java | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java index 52de93c47d..a18f7dab3f 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java @@ -1,10 +1,12 @@ package io.stargate.sgv2.jsonapi.api.v1; import static io.stargate.sgv2.jsonapi.api.v1.ResponseAssertions.responseIsError; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.stargate.sgv2.jsonapi.exception.EmbeddingProviderException; import io.stargate.sgv2.jsonapi.exception.RequestException; import io.stargate.sgv2.jsonapi.exception.SchemaException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; @@ -267,8 +269,8 @@ void failOnRerankOverrideEolModel() { @Test void overrideWithAuthPassesValidation() { // Override with valid provider+model and authentication credentials. - // Should pass all override validation and fail downstream (e.g., embedding provider), - // NOT with a rerank validation error. + // Override validation passes; pipeline proceeds to embedding (vectorize) step which + // fails with EMBEDDING_PROVIDER_CLIENT_ERROR (no real OpenAI API key in test env). String collectionName = "rerank_override_with_auth"; createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); @@ -283,17 +285,13 @@ void overrideWithAuthPassesValidation() { .body("$", responseIsError()) .body( "errors[0].errorCode", - not( - anyOf( - is(RequestException.Code.INVALID_RERANK_OVERRIDE.name()), - is(RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.name())))); + is(EmbeddingProviderException.Code.EMBEDDING_PROVIDER_CLIENT_ERROR.name())); } @Test void overrideWithAuthAndParametersPassesValidation() { // Override with valid provider+model, authentication, and parameters. - // Should pass all override validation and fail downstream, - // NOT with a rerank validation error. + // Override validation passes; fails downstream at embedding step. String collectionName = "rerank_override_auth_params"; createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); @@ -309,10 +307,28 @@ void overrideWithAuthAndParametersPassesValidation() { .body("$", responseIsError()) .body( "errors[0].errorCode", - not( - anyOf( - is(RequestException.Code.INVALID_RERANK_OVERRIDE.name()), - is(RequestException.Code.UNSUPPORTED_RERANKING_COMMAND.name())))); + is(EmbeddingProviderException.Code.EMBEDDING_PROVIDER_CLIENT_ERROR.name())); + } + + @Test + void overrideWithParametersOnlyPassesValidation() { + // Override with valid provider+model and parameters but no authentication. + // Override validation passes; fails downstream at embedding step. + String collectionName = "rerank_override_params_only"; + createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); + + givenHeadersPostJsonThen( + keyspaceName, + collectionName, + findAndRerankWithOverride( + """ + {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", + "parameters": {"truncate": "END"}} + """)) + .body("$", responseIsError()) + .body( + "errors[0].errorCode", + is(EmbeddingProviderException.Code.EMBEDDING_PROVIDER_CLIENT_ERROR.name())); } /** From 1eae708aeea09497e5c96a1d0051e27606bd5b7a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 29 Apr 2026 15:26:04 -0700 Subject: [PATCH 16/18] Simplify tests --- ...indAndRerankCollectionIntegrationTest.java | 74 +++++++---------- .../FindAndRerankOperationBuilderTest.java | 80 ++++++------------- 2 files changed, 51 insertions(+), 103 deletions(-) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java index a18f7dab3f..8f869c4613 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java @@ -268,63 +268,45 @@ void failOnRerankOverrideEolModel() { @Test void overrideWithAuthPassesValidation() { - // Override with valid provider+model and authentication credentials. - // Override validation passes; pipeline proceeds to embedding (vectorize) step which - // fails with EMBEDDING_PROVIDER_CLIENT_ERROR (no real OpenAI API key in test env). - String collectionName = "rerank_override_with_auth"; - createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); - - givenHeadersPostJsonThen( - keyspaceName, - collectionName, - findAndRerankWithOverride( - """ - {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", - "authentication": {"providerKey": "my-test-key"}} - """)) - .body("$", responseIsError()) - .body( - "errors[0].errorCode", - is(EmbeddingProviderException.Code.EMBEDDING_PROVIDER_CLIENT_ERROR.name())); + overridePassesValidation( + "rerank_override_with_auth", + """ + {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", + "authentication": {"providerKey": "my-test-key"}} + """); } @Test void overrideWithAuthAndParametersPassesValidation() { - // Override with valid provider+model, authentication, and parameters. - // Override validation passes; fails downstream at embedding step. - String collectionName = "rerank_override_auth_params"; - createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); - - givenHeadersPostJsonThen( - keyspaceName, - collectionName, - findAndRerankWithOverride( - """ - {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", - "authentication": {"providerKey": "my-test-key"}, - "parameters": {"truncate": "END"}} - """)) - .body("$", responseIsError()) - .body( - "errors[0].errorCode", - is(EmbeddingProviderException.Code.EMBEDDING_PROVIDER_CLIENT_ERROR.name())); + overridePassesValidation( + "rerank_override_auth_params", + """ + {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", + "authentication": {"providerKey": "my-test-key"}, + "parameters": {"truncate": "END"}} + """); } @Test void overrideWithParametersOnlyPassesValidation() { - // Override with valid provider+model and parameters but no authentication. - // Override validation passes; fails downstream at embedding step. - String collectionName = "rerank_override_params_only"; + overridePassesValidation( + "rerank_override_params_only", + """ + {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", + "parameters": {"truncate": "END"}} + """); + } + + /** + * Verifies that a valid rerank override passes validation and the pipeline proceeds to the + * embedding (vectorize) step, which fails with EMBEDDING_PROVIDER_CLIENT_ERROR because there is + * no real OpenAI API key in the test environment. + */ + private void overridePassesValidation(String collectionName, String rerankOverrideJson) { createCollectionWithCleanup(collectionName, VECTORIZE_NO_RERANK_SPEC); givenHeadersPostJsonThen( - keyspaceName, - collectionName, - findAndRerankWithOverride( - """ - {"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2", - "parameters": {"truncate": "END"}} - """)) + keyspaceName, collectionName, findAndRerankWithOverride(rerankOverrideJson)) .body("$", responseIsError()) .body( "errors[0].errorCode", diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java index 3776055afe..0a128d1574 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java @@ -70,28 +70,26 @@ private static RerankingProvidersConfig configWithProvider( @Nested class ValidateRerankOverride { + // Shared config: nvidia provider enabled with a single supported model + private final RerankingProvidersConfig NVIDIA_SUPPORTED = + configWithProvider( + "nvidia", + true, + List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); + @Test void shouldAcceptSupportedProviderAndModel() { - var config = - configWithProvider( - "nvidia", - true, - List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); - - assertThatCode(() -> builder.validateRerankOverride(config, "nvidia", "nvidia/rerank-v1")) + assertThatCode( + () -> builder.validateRerankOverride(NVIDIA_SUPPORTED, "nvidia", "nvidia/rerank-v1")) .doesNotThrowAnyException(); } @Test void shouldRejectUnknownProvider() { - var config = - configWithProvider( - "nvidia", - true, - List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); - assertThatThrownBy( - () -> builder.validateRerankOverride(config, "unknown-provider", "some-model")) + () -> + builder.validateRerankOverride( + NVIDIA_SUPPORTED, "unknown-provider", "some-model")) .isInstanceOf(RequestException.class) .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) .hasMessageContaining("unknown-provider"); @@ -99,13 +97,14 @@ void shouldRejectUnknownProvider() { @Test void shouldRejectDisabledProvider() { - var config = + var disabledConfig = configWithProvider( "nvidia", false, List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); - assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", "nvidia/rerank-v1")) + assertThatThrownBy( + () -> builder.validateRerankOverride(disabledConfig, "nvidia", "nvidia/rerank-v1")) .isInstanceOf(RequestException.class) .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) .hasMessageContaining("disabled"); @@ -113,13 +112,7 @@ void shouldRejectDisabledProvider() { @Test void shouldRejectNullModelName() { - var config = - configWithProvider( - "nvidia", - true, - List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); - - assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", null)) + assertThatThrownBy(() -> builder.validateRerankOverride(NVIDIA_SUPPORTED, "nvidia", null)) .isInstanceOf(RequestException.class) .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) .hasMessageContaining("modelName"); @@ -127,14 +120,10 @@ void shouldRejectNullModelName() { @Test void shouldRejectUnknownModel() { - var config = - configWithProvider( - "nvidia", - true, - List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); - assertThatThrownBy( - () -> builder.validateRerankOverride(config, "nvidia", "nvidia/nonexistent-model")) + () -> + builder.validateRerankOverride( + NVIDIA_SUPPORTED, "nvidia", "nvidia/nonexistent-model")) .isInstanceOf(RequestException.class) .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) .hasMessageContaining("nonexistent-model"); @@ -166,35 +155,12 @@ void shouldRejectEndOfLifeModel() { .hasFieldOrPropertyWithValue("code", SchemaException.Code.END_OF_LIFE_AI_MODEL.name()); } - @Test - void shouldRejectNullModelNameBeforeCheckingModelExistence() { - // Even when the provider has models, null modelName should be caught - // before the stream filter (which would NPE on .equals(null)) - var config = - configWithProvider( - "nvidia", - true, - List.of( - modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED), - modelConfig("nvidia/rerank-v2", ApiModelSupport.SupportStatus.SUPPORTED))); - - assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", null)) - .isInstanceOf(RequestException.class) - .hasFieldOrPropertyWithValue( - "code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()); - } - @Test void shouldRejectUnknownProviderBeforeCheckingModelName() { // When both provider is unknown AND modelName is null, the provider check should // come first — user gets the more actionable "provider not supported" error - var config = - configWithProvider( - "nvidia", - true, - List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); - - assertThatThrownBy(() -> builder.validateRerankOverride(config, "unknown-provider", null)) + assertThatThrownBy( + () -> builder.validateRerankOverride(NVIDIA_SUPPORTED, "unknown-provider", null)) .isInstanceOf(RequestException.class) .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) .hasMessageContaining("unknown-provider") @@ -205,13 +171,13 @@ void shouldRejectUnknownProviderBeforeCheckingModelName() { void shouldRejectDisabledProviderBeforeCheckingModelName() { // When provider is disabled AND modelName is null, the disabled check should // come first — user gets "provider disabled" instead of "modelName required" - var config = + var disabledConfig = configWithProvider( "nvidia", false, List.of(modelConfig("nvidia/rerank-v1", ApiModelSupport.SupportStatus.SUPPORTED))); - assertThatThrownBy(() -> builder.validateRerankOverride(config, "nvidia", null)) + assertThatThrownBy(() -> builder.validateRerankOverride(disabledConfig, "nvidia", null)) .isInstanceOf(RequestException.class) .hasFieldOrPropertyWithValue("code", RequestException.Code.INVALID_RERANK_OVERRIDE.name()) .hasMessageContaining("disabled"); From b6afdc2b0a0817171eaa119b3433c9326b9b60f8 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 30 Apr 2026 09:57:03 -0700 Subject: [PATCH 17/18] Fix failing IT --- .../jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java index 8f869c4613..23df15d234 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java @@ -184,6 +184,9 @@ private void errorOnNotEnabled( "provider": "openai", "modelName": "text-embedding-3-small" } + }, + "rerank": { + "enabled": false } } } From f2afee05be23d4cb02d702fb89969e36fbc90dd2 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 30 Apr 2026 09:58:59 -0700 Subject: [PATCH 18/18] Remove unused helper method --- .../java/io/stargate/sgv2/jsonapi/util/StringUtil.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java b/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java index 9bd6ddb96f..9e34040f8d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/util/StringUtil.java @@ -13,12 +13,4 @@ public static String normalizeOptionalString(String string) { public static String normalizeOptionalString(Optional string) { return normalizeOptionalString(string.orElse("")); } - - /** - * Returns null if the string is null, empty, or contains only whitespace; otherwise String - * itself. - */ - public static String blankToNull(String value) { - return (value != null && value.isBlank()) ? null : value; - } }