Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5384f34
Add SyncService client from EGW
tatu-at-datastax Feb 21, 2026
f545b4f
...
tatu-at-datastax Feb 21, 2026
51cede3
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Feb 24, 2026
7974351
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Feb 25, 2026
36f330d
Minor comment addition
tatu-at-datastax Feb 25, 2026
b3d9689
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Feb 27, 2026
15a44fe
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Feb 27, 2026
99d7663
Add wrapper to get shared secret from secret service in "embedded EGW…
tatu-at-datastax Feb 27, 2026
3b3e843
refactoring: egw->syncservice
tatu-at-datastax Feb 27, 2026
657913d
Add warning for missing/mismatching credentials
tatu-at-datastax Feb 28, 2026
32dc72d
Fix resolution logic
tatu-at-datastax Mar 2, 2026
6f71b26
One more fix
tatu-at-datastax Mar 2, 2026
9cc09f1
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 4, 2026
562ce0b
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 4, 2026
1bdcb05
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 10, 2026
6559e27
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 10, 2026
7084913
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 13, 2026
781e13a
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 17, 2026
9553eb1
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 17, 2026
f10166e
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 30, 2026
3c319ab
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Mar 31, 2026
ec0a8ea
Fix minor regression
tatu-at-datastax Mar 31, 2026
df809e3
Fix issues from code review
tatu-at-datastax Mar 31, 2026
0b54a64
./mvnw fmt:format
tatu-at-datastax Mar 31, 2026
aecb79f
Add some testing
tatu-at-datastax Mar 31, 2026
ac1a67a
Try disabling failing IT
tatu-at-datastax Mar 31, 2026
46b7be6
Remove test that fails due to needing Sync Service
tatu-at-datastax Mar 31, 2026
f029481
Put failing test back, but now disabled
tatu-at-datastax Mar 31, 2026
c26148d
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Apr 1, 2026
e5e4743
Merge branch 'main' into tatu/2387-add-egw-sync-service-client
tatu-at-datastax Apr 1, 2026
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
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-docker</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@ public class DataVectorizer {
/**
* Constructor
*
* @param embeddingProvider - Service client based on embedding service configuration set for the
* table
* @param embeddingProviderWrapper - Service client based on embedding service configuration set
* for the table
* @param nodeFactory - Jackson node factory to create json nodes added to the document
* @param embeddingCredentials - Credentials for the embedding service
* @param schemaObject - The collection setting for vectorize call
*/
public DataVectorizer(
MeteredEmbeddingProviderWrapper embeddingProvider,
MeteredEmbeddingProviderWrapper embeddingProviderWrapper,
JsonNodeFactory nodeFactory,
EmbeddingCredentials embeddingCredentials,
SchemaObject schemaObject) {
// 16-Feb-2026, tatu: This can be null, apparently
this.embeddingProviderWrapper = embeddingProvider;
this.embeddingProviderWrapper = embeddingProviderWrapper;
this.nodeFactory = nodeFactory;
this.embeddingCredentials =
Objects.requireNonNull(embeddingCredentials, "embeddingCredentials must not be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.stargate.sgv2.jsonapi.service.embedding.configuration.ServiceConfigStore;
import io.stargate.sgv2.jsonapi.service.embedding.gateway.EmbeddingGatewayClient;
import io.stargate.sgv2.jsonapi.service.provider.ModelProvider;
import io.stargate.sgv2.jsonapi.syncservice.SyncServiceClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
Expand All @@ -29,6 +30,8 @@ public class EmbeddingProviderFactory {
@GrpcClient("embedding")
EmbeddingService grpcGatewayClient;

@Inject SyncServiceClient syncServiceClient;

@FunctionalInterface
interface ProviderConstructor {
EmbeddingProvider create(
Expand Down Expand Up @@ -99,7 +102,7 @@ public EmbeddingProvider create(
commandName);
}

public EmbeddingProvider create(
private EmbeddingProvider create(
Tenant tenant,
String authToken,
ModelProvider modelProvider,
Expand Down Expand Up @@ -190,7 +193,17 @@ public EmbeddingProvider create(
"ModelProvider does not have a constructor: " + modelProvider);
}

return ctor.create(
providerConfig, modelConfig, serviceConfig, dimension, vectorizeServiceParameters);
var provider =
ctor.create(
providerConfig, modelConfig, serviceConfig, dimension, vectorizeServiceParameters);

// Wrap with credential resolver if shared-secret authentication is configured
if (authentication != null && !authentication.isEmpty()) {
provider =
new SyncServiceCredentialResolvingProvider(
provider, syncServiceClient, authentication, tenant, authToken);
}

return provider;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package io.stargate.sgv2.jsonapi.service.embedding.operation;

import io.smallrye.mutiny.Uni;
import io.stargate.sgv2.jsonapi.api.request.EmbeddingCredentials;
import io.stargate.sgv2.jsonapi.api.request.tenant.Tenant;
import io.stargate.sgv2.jsonapi.syncservice.SyncServiceClient;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A decorator that wraps a direct {@link EmbeddingProvider} and resolves shared-secret credentials
* via {@link SyncServiceClient} before each {@link #vectorize} call.
*
* <p>When the Embedding Gateway (EGW) is disabled (standalone mode), collections configured with
* {@code SHARED_SECRET} authentication need their credential names resolved into actual secrets.
* This provider handles that resolution by calling {@link SyncServiceClient#getCredential} for each
* entry in the authentication map, then passing the resolved credentials to the delegate provider.
*/
public class SyncServiceCredentialResolvingProvider extends EmbeddingProvider {

private static final Logger LOGGER =
LoggerFactory.getLogger(SyncServiceCredentialResolvingProvider.class);

private static final String PROVIDER_KEY = "providerKey";
private static final String ACCESS_ID = "accessId";
private static final String SECRET_KEY = "secretKey";
private static final String SECRET_ID = "secretId";

private final EmbeddingProvider delegate;
private final SyncServiceClient syncServiceClient;
private final Map<String, String> authentication;
private final Tenant tenant;
private final String authToken;

public SyncServiceCredentialResolvingProvider(
EmbeddingProvider delegate,
SyncServiceClient syncServiceClient,
Map<String, String> authentication,
Tenant tenant,
String authToken) {
super(
delegate.modelProvider(),
delegate.providerConfig,
delegate.modelConfig,
delegate.serviceConfig,
delegate.dimension,
delegate.vectorizeServiceParameters);

this.delegate = delegate;
this.syncServiceClient = syncServiceClient;
this.authentication = authentication;
this.tenant = tenant;
this.authToken = authToken;
}

@Override
protected String errorMessageJsonPtr() {
// Not used directly — this wrapper never makes HTTP calls itself
return "";
}

@Override
public Uni<BatchedEmbeddingResponse> vectorize(
int batchId,
List<String> texts,
EmbeddingCredentials embeddingCredentials,
EmbeddingRequestType embeddingRequestType) {

// Match EGW behavior: if caller already provided credentials via headers, use those
// directly and skip SyncService resolution
if (hasHeaderCredentials(embeddingCredentials)) {
return delegate.vectorize(batchId, texts, embeddingCredentials, embeddingRequestType);
}

return resolveCredentials()
.flatMap(resolved -> delegate.vectorize(batchId, texts, resolved, embeddingRequestType));
}

private static boolean hasHeaderCredentials(EmbeddingCredentials creds) {
return creds.apiKey().isPresent()
|| creds.accessId().isPresent()
|| creds.secretId().isPresent();
}

@Override
public int maxBatchSize() {
return delegate.maxBatchSize();
}

/**
* Resolves credentials by calling SyncService for each entry in the authentication map. Each
* entry's key is the accepted token name (e.g. "providerKey"), and the value is the credential
* reference name stored in SyncService (e.g. "my-openai-cred").
*
* <p>The SyncService response map is keyed by the credential reference name, so we extract the
* resolved secret using the credential name as key (matching EGW's EmbeddingServiceImpl
* behavior).
*/
private Uni<EmbeddingCredentials> resolveCredentials() {
String providerName = modelProvider().apiName();

// Build parallel SyncService calls and track which accepted token name each one is for
List<String> acceptedNames = new ArrayList<>();
List<String> credNames = new ArrayList<>();
List<Uni<Map<String, String>>> resolveUnis = new ArrayList<>();

for (Map.Entry<String, String> entry : authentication.entrySet()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great improvement! Original code only resolves one cred

acceptedNames.add(entry.getKey());
credNames.add(entry.getValue());
resolveUnis.add(
syncServiceClient.getCredential(authToken, tenant, providerName, entry.getValue()));
}

return Uni.join()
.all(resolveUnis)
.andFailFast()
.map(
results -> {
// For each resolved result, extract the secret using the credential name as key
// (SyncService response is keyed by credential reference name, not accepted name)
Map<String, String> resolvedByAcceptedName = new HashMap<>();
for (int i = 0; i < results.size(); i++) {
Map<String, String> credMap = results.get(i);
if (credMap != null) {
String credName = credNames.get(i);
String acceptedName = acceptedNames.get(i);
String resolvedValue = credMap.get(credName);
if (resolvedValue != null) {
resolvedByAcceptedName.put(acceptedName, resolvedValue);
} else {
LOGGER.warn(
"SyncService response for credential '{}' (provider '{}') did not contain"
+ " expected key '{}'; available keys: {}",
credName,
providerName,
credName,
credMap.keySet());
}
}
}
return buildEmbeddingCredentials(resolvedByAcceptedName);
});
}

/**
* Maps resolved credential values (keyed by accepted token name) to {@link EmbeddingCredentials}.
* The accepted token names come from the provider's SHARED_SECRET config (e.g. "providerKey",
* "accessId", "secretKey").
*/
private EmbeddingCredentials buildEmbeddingCredentials(
Map<String, String> resolvedByAcceptedName) {
var apiKey = Optional.ofNullable(resolvedByAcceptedName.get(PROVIDER_KEY));
var accessId = Optional.ofNullable(resolvedByAcceptedName.get(ACCESS_ID));
var secretId =
Optional.ofNullable(
resolvedByAcceptedName.containsKey(SECRET_KEY)
? resolvedByAcceptedName.get(SECRET_KEY)
: resolvedByAcceptedName.get(SECRET_ID));
return new EmbeddingCredentials(tenant, apiKey, accessId, secretId, Optional.of(authToken));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.stargate.sgv2.jsonapi.service.embedding.configuration.EmbeddingProvidersConfig;
import io.stargate.sgv2.jsonapi.service.provider.ApiModelSupport;
import io.stargate.sgv2.jsonapi.service.provider.ModelProvider;
import io.stargate.sgv2.jsonapi.syncservice.SyncServiceClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
Expand All @@ -26,15 +27,18 @@ public class VectorizeConfigValidator {
private final OperationsConfig operationsConfig;
private final EmbeddingProvidersConfig embeddingProvidersConfig;
private final ValidateCredentials validateCredentials;
private final SyncServiceClient syncServiceClient;

@Inject
public VectorizeConfigValidator(
OperationsConfig operationsConfig,
EmbeddingProvidersConfig embeddingProvidersConfig,
ValidateCredentials validateCredentials) {
ValidateCredentials validateCredentials,
SyncServiceClient syncServiceClient) {
this.operationsConfig = operationsConfig;
this.embeddingProvidersConfig = embeddingProvidersConfig;
this.validateCredentials = validateCredentials;
this.syncServiceClient = syncServiceClient;
}

/**
Expand Down Expand Up @@ -189,8 +193,11 @@ private void validateAuthentication(

// Validate the credential name from secret service
// already append the .providerKey to the value in CreateCollectionCommand
// Both validate() and validateKey() are blocking and throw on invalid credentials
if (operationsConfig.enableEmbeddingGateway()) {
validateCredentials.validate(userConfig.provider(), userAuth.getValue());
} else {
syncServiceClient.validateKey(userConfig.provider(), userAuth.getValue());
}
}
}
Expand Down
Loading
Loading