Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,17 @@ 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. Completely replaces the"
+ " collection-level reranking configuration when provided. Both provider and"
+ " modelName are required.",
implementation = CreateCollectionCommand.Options.RerankServiceDesc.class)
@JsonProperty("rerank")
CreateCollectionCommand.Options.RerankServiceDesc rerankServiceOverride) {}

@JsonDeserialize(using = HybridLimitsDeserializer.class)
public record HybridLimits(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public enum Code implements ErrorCode<RequestException> {
HYBRID_FIELD_UNSUPPORTED_SUBFIELD_VALUE_TYPE,

INVALID_CREATE_COLLECTION_FIELD,
INVALID_RERANK_OVERRIDE,
MISSING_RERANK_QUERY_TEXT,

REQUEST_NOT_JSON,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -56,6 +58,9 @@ class FindAndRerankOperationBuilder {
private FindAndRerankCommand command;
private FindCommandResolver findCommandResolver;

// lazily computed effective rerank service def (per-request override or collection default)
private CollectionRerankDef.RerankServiceDef effectiveRerankServiceDef;

public FindAndRerankOperationBuilder(CommandContext<CollectionSchemaObject> commandContext) {
this.commandContext = Objects.requireNonNull(commandContext, "commandContext cannot be null");

Expand Down Expand Up @@ -160,45 +165,63 @@ private void checkSupported() {
}
}

if (!commandContext.schemaObject().rerankingConfig().enabled()) {
// TODO: more info in the error
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();
Comment thread
tatu-at-datastax marked this conversation as resolved.
}
// 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());
// 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).")));

// 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(
"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 TaskGroupAndDeferrables<RerankingTask<CollectionSchemaObject>, CollectionSchemaObject>
rerankTasks(List<RerankingTask.DeferredCommandWithSource> deferredCommandResults) {

// Previous code will check reranking is supported
var providerConfig = commandContext.schemaObject().rerankingConfig().rerankServiceDef();
// 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
Expand Down Expand Up @@ -238,6 +261,63 @@ private void checkSupported() {
.collect(java.util.stream.Collectors.toUnmodifiableList()));
}

/**
* Validates that the overridden provider and model exist and are usable in the reranking
* providers configuration.
*/
// package-private for unit testing
void validateRerankOverride(
RerankingProvidersConfig rerankingProvidersConfig, String provider, String modelName) {
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));
}
// 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))
.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));
}

// 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<IntermediateCollectionReadTask, CollectionSchemaObject> readTasks(
RerankingTask.DeferredCommandWithSource deferredVectorRead,
RerankingTask.DeferredCommandWithSource deferredBM25Read) {
Expand Down
20 changes: 15 additions & 5 deletions src/main/resources/errors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,21 @@ 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
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:
Expand Down
Loading
Loading