Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ private AIConfig buildConfig(
String graphKey) {
Supplier<LDAIConfigTracker> factory = trackerFactory(
key, parsed.getVariationKey(), parsed.getVersion(),
parsed.getModel(), parsed.getProvider(), context, graphKey);
parsed.getModel(), parsed.getProvider(),
parsed.getModelKey(), parsed.getModelVersion(), context, graphKey);
switch (mode) {
case AGENT:
return new AIAgentConfig(
Expand Down Expand Up @@ -330,7 +331,7 @@ private AIConfig buildConfigFromDefault(
String graphKey) {
// Default configs still get real trackers — the configKey was requested even if no flag was found.
// variationKey is null because no flag evaluation occurred.
Supplier<LDAIConfigTracker> factory = trackerFactory(key, null, null, null, null, context, graphKey);
Supplier<LDAIConfigTracker> factory = trackerFactory(key, null, null, null, null, null, null, context, graphKey);
switch (mode) {
case AGENT: {
AIAgentConfigDefault agent = (AIAgentConfigDefault) defaultValue;
Expand Down Expand Up @@ -386,21 +387,14 @@ private Supplier<LDAIConfigTracker> trackerFactory(
Integer version,
com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Model model,
com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Provider provider,
LDContext context) {
return trackerFactory(configKey, variationKey, version, model, provider, context, null);
}

private Supplier<LDAIConfigTracker> trackerFactory(
String configKey,
String variationKey,
Integer version,
com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Model model,
com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Provider provider,
String modelKey,
Integer modelVersion,
LDContext context,
String graphKey) {
String modelName = model != null && model.getName() != null ? model.getName() : "";
String providerName = provider != null && provider.getName() != null ? provider.getName() : "";
int ver = version != null ? version : 1;
int mVer = modelVersion != null ? modelVersion : 1;
return () -> new LDAIConfigTrackerImpl(
client,
UUID.randomUUID().toString(),
Expand All @@ -409,6 +403,8 @@ private Supplier<LDAIConfigTracker> trackerFactory(
ver,
modelName,
providerName,
modelKey,
mVer,
context,
graphKey,
logger);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ public static final class Model {
private final Map<String, Object> parameters;
private final Map<String, Object> custom;

private Model(String name, Map<String, Object> parameters, Map<String, Object> custom) {
private Model(
String name,
Map<String, Object> parameters,
Map<String, Object> custom) {
this.name = name;
this.parameters = parameters;
this.custom = custom;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,8 @@ public static final class TrackData {
private final int version;
private final String modelName;
private final String providerName;
private final String modelKey;
private final int modelVersion;
private final String graphKey;

/**
Expand All @@ -610,6 +612,8 @@ public static final class TrackData {
* @param version the config version
* @param modelName the model name, or empty string when unknown
* @param providerName the provider name, or empty string when unknown
* @param modelKey the stable model key, or {@code null} when unknown
* @param modelVersion the model version
* @param graphKey the agent graph key, or {@code null} when not part of a graph
*/
public TrackData(
Expand All @@ -619,13 +623,17 @@ public TrackData(
int version,
String modelName,
String providerName,
String modelKey,
int modelVersion,
String graphKey) {
this.runId = Objects.requireNonNull(runId, "runId");
this.configKey = Objects.requireNonNull(configKey, "configKey");
this.variationKey = variationKey;
this.version = version;
this.modelName = modelName == null ? "" : modelName;
this.providerName = providerName == null ? "" : providerName;
this.modelKey = modelKey == null || modelKey.trim().isEmpty() ? null : modelKey;
this.modelVersion = modelVersion;
this.graphKey = graphKey;
}

Expand Down Expand Up @@ -683,6 +691,24 @@ public String getProviderName() {
return providerName;
}

/**
* Returns the stable model key.
*
* @return the model key, or {@code null} when unknown
*/
public String getModelKey() {
return modelKey;
}

/**
* Returns the model version.
*
* @return the model version
*/
public int getModelVersion() {
return modelVersion;
}

/**
* Returns the agent graph key.
*
Expand All @@ -695,7 +721,7 @@ public String getGraphKey() {
/**
* Builds an {@link LDValue} representation of this track data using camelCase keys.
* <p>
* {@code variationKey} and {@code graphKey} are omitted when {@code null}.
* {@code variationKey}, {@code modelKey}, and {@code graphKey} are omitted when {@code null}.
*
* @return an {@link LDValue} object containing all non-null fields
*/
Expand All @@ -705,10 +731,14 @@ public LDValue toLDValue() {
.put("configKey", configKey)
.put("version", version)
.put("modelName", modelName)
.put("providerName", providerName);
.put("providerName", providerName)
.put("modelVersion", modelVersion);
if (variationKey != null) {
b.put("variationKey", variationKey);
}
if (modelKey != null) {
b.put("modelKey", modelKey);
}
if (graphKey != null) {
b.put("graphKey", graphKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public final class AIConfigFlagValue {
private final Map<String, Tool> tools;
private final JudgeConfiguration judgeConfiguration;
private final String evaluationMetricKey;
private final String modelKey;
private final Integer modelVersion;

private AIConfigFlagValue(Builder b) {
this.enabled = b.enabled;
Expand All @@ -50,6 +52,8 @@ private AIConfigFlagValue(Builder b) {
this.tools = b.tools == null ? null : Collections.unmodifiableMap(b.tools);
this.judgeConfiguration = b.judgeConfiguration;
this.evaluationMetricKey = b.evaluationMetricKey;
this.modelKey = b.modelKey;
this.modelVersion = b.modelVersion;
}

/**
Expand Down Expand Up @@ -97,6 +101,26 @@ public Mode getMode() {
return mode;
}

/**
* Returns the {@code _ldMeta.modelKey}. Lives under {@code _ldMeta} rather than {@code model}
* itself, to avoid {@code modelVersion} reading as the underlying LLM's own version
* (e.g. "GPT-5.4").
*
* @return the model key, or {@code null} if absent
*/
public String getModelKey() {
return modelKey;
}

/**
* Returns the {@code _ldMeta.modelVersion}.
*
* @return the model version, or {@code null} if absent
*/
public Integer getModelVersion() {
return modelVersion;
}

/**
* Returns the model configuration.
*
Expand Down Expand Up @@ -184,6 +208,8 @@ public static final class Builder {
private Map<String, Tool> tools;
private JudgeConfiguration judgeConfiguration;
private String evaluationMetricKey;
private String modelKey;
private Integer modelVersion;

private Builder() {
}
Expand Down Expand Up @@ -221,6 +247,28 @@ public Builder version(Integer v) {
return this;
}

/**
* Sets the model key, parsed from {@code _ldMeta.modelKey}.
*
* @param v the model key
* @return this builder
*/
public Builder modelKey(String v) {
this.modelKey = v;
return this;
}

/**
* Sets the model version, parsed from {@code _ldMeta.modelVersion}.
*
* @param v the model version
* @return this builder
*/
public Builder modelVersion(Integer v) {
this.modelVersion = v;
return this;
}

/**
* Sets the mode.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public static AIConfigFlagValue parse(LDValue value) {
return builder.build();
}

parseMeta(value.get("_ldMeta"), builder);
LDValue meta = value.get("_ldMeta");
parseMeta(meta, builder);
builder.model(parseModel(value.get("model")));
builder.provider(parseProvider(value.get("provider")));
builder.messages(parseMessages(value.get("messages")));
Expand All @@ -54,8 +55,13 @@ public static AIConfigFlagValue parse(LDValue value) {
return builder.build();
}

// modelKey/modelVersion are parsed here alongside the rest of _ldMeta (enabled, variationKey,
// version, mode) rather than inside parseModel, so they're stored the same way as every other
// _ldMeta field instead of a one-off path. They also live under _ldMeta rather than model
// itself, to avoid modelVersion reading as the underlying LLM's own version (e.g. "GPT-5.4").
private static void parseMeta(LDValue meta, AIConfigFlagValue.Builder builder) {
if (meta == null || meta.getType() != LDValueType.OBJECT) {
builder.modelVersion(1);
return;
}
LDValue enabled = meta.get("enabled");
Expand All @@ -68,6 +74,10 @@ private static void parseMeta(LDValue meta, AIConfigFlagValue.Builder builder) {
builder.version(version.intValue());
}
builder.mode(Mode.fromWireValue(asStringOrNull(meta.get("mode"))));

LDValue modelVersion = meta.get("modelVersion");
builder.modelVersion(modelVersion.getType() == LDValueType.NUMBER ? modelVersion.intValue() : 1);
builder.modelKey(trimToNull(asStringOrNull(meta.get("modelKey"))));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public final class LDAIConfigTrackerImpl implements LDAIConfigTracker {
private final int version;
private final String modelName; // empty string when unknown
private final String providerName; // empty string when unknown
private final String modelKey; // nullable
private final int modelVersion;
private final String graphKey; // nullable

// Computed once at construction
Expand Down Expand Up @@ -86,6 +88,8 @@ public final class LDAIConfigTrackerImpl implements LDAIConfigTracker {
* @param version the config version
* @param modelName the model name, or empty string when unknown
* @param providerName the provider name, or empty string when unknown
* @param modelKey the stable model key, or {@code null} when unknown
* @param modelVersion the model version
* @param context the evaluation context; must not be {@code null}
* @param graphKey the agent graph key, or {@code null} when not part of a graph
* @param logger the logger; must not be {@code null}
Expand All @@ -98,6 +102,8 @@ public LDAIConfigTrackerImpl(
int version,
String modelName,
String providerName,
String modelKey,
int modelVersion,
LDContext context,
String graphKey,
LDLogger logger) {
Expand All @@ -108,6 +114,8 @@ public LDAIConfigTrackerImpl(
this.version = version;
this.modelName = modelName == null ? "" : modelName;
this.providerName = providerName == null ? "" : providerName;
this.modelKey = modelKey == null || modelKey.trim().isEmpty() ? null : modelKey;
this.modelVersion = modelVersion;
this.context = Objects.requireNonNull(context, "context");
this.graphKey = graphKey;
this.logger = Objects.requireNonNull(logger, "logger");
Expand Down Expand Up @@ -137,14 +145,25 @@ public static LDAIConfigTrackerImpl fromResumptionToken(
d.getVersion(),
"", // modelName not carried in token
"", // providerName not carried in token
null, // modelKey not carried in token
1, // modelVersion not carried in token
context,
d.getGraphKey(),
logger);
}

@Override
public TrackData getTrackData() {
return new TrackData(runId, configKey, variationKey, version, modelName, providerName, graphKey);
return new TrackData(
runId,
configKey,
variationKey,
version,
modelName,
providerName,
modelKey,
modelVersion,
graphKey);
}

@Override
Expand Down Expand Up @@ -377,10 +396,14 @@ private ObjectBuilder baseData() {
.put("configKey", configKey)
.put("version", version)
.put("modelName", modelName)
.put("providerName", providerName);
.put("providerName", providerName)
.put("modelVersion", modelVersion);
if (variationKey != null) {
b.put("variationKey", variationKey);
}
if (modelKey != null) {
b.put("modelKey", modelKey);
}
if (graphKey != null) {
b.put("graphKey", graphKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,33 @@ public void completionConfigReturnsTypedConfigFromVariation() {
assertThat(config.getMessages().get(0).getRole(), is(Message.Role.SYSTEM));
}

@Test
public void completionConfigPropagatesModelMetadataToTracker() {
String json = "{\"_ldMeta\":{\"enabled\":true,\"mode\":\"completion\","
+ "\"modelKey\":\"custom-gpt\",\"modelVersion\":7},"
+ "\"model\":{\"name\":\"gpt-4\"}}";
when(client.jsonValueVariation(anyString(), any(), any())).thenReturn(LDValue.parse(json));

AICompletionConfig config = ai.completionConfig("key", context, null, null);

// modelKey/modelVersion are intentionally not exposed on Model; they surface only via the
// tracker's stamped event data, mirroring variationKey/version.
assertThat(config.createTracker().getTrackData().getModelKey(), is("custom-gpt"));
assertThat(config.createTracker().getTrackData().getModelVersion(), is(7));
}

@Test
public void completionConfigDefaultsMissingModelMetadata() {
String json = "{\"_ldMeta\":{\"enabled\":true,\"mode\":\"completion\"},"
+ "\"model\":{\"name\":\"gpt-4\"}}";
when(client.jsonValueVariation(anyString(), any(), any())).thenReturn(LDValue.parse(json));

AICompletionConfig config = ai.completionConfig("key", context, null, null);

assertThat(config.createTracker().getTrackData().getModelKey(), is(nullValue()));
assertThat(config.createTracker().getTrackData().getModelVersion(), is(1));
}

@Test
public void interpolationExposesContextAsLdctx() {
String json = "{\"_ldMeta\":{\"enabled\":true,\"mode\":\"completion\"},"
Expand Down
Loading
Loading