+ * {@code LDAIClient} is the gateway to AI Config functionality. It wraps a fully-configured
+ * {@link LDClientInterface} (such as an {@code com.launchdarkly.sdk.server.LDClient}) and uses it to
+ * evaluate AI Config flags, interpolate prompts, and record AI metrics.
+ *
+ * Construct one instance per application, reusing your existing LaunchDarkly client:
+ *
+ */
+public final class LDAIClient {
+ private static final String TRACK_SDK_INFO = "$ld:ai:sdk:info";
+ private static final String TRACK_USAGE_COMPLETION_CONFIG = "$ld:ai:usage:completion-config";
+ private static final String TRACK_USAGE_AGENT_CONFIG = "$ld:ai:usage:agent-config";
+ private static final String TRACK_USAGE_AGENT_CONFIGS = "$ld:ai:usage:agent-configs";
+ private static final String TRACK_USAGE_JUDGE_CONFIG = "$ld:ai:usage:judge-config";
+
+ private static final LDContext INIT_TRACK_CONTEXT = LDContext.builder("ld-internal-tracking")
+ .kind("ld_ai")
+ .anonymous(true)
+ .build();
+
+ private final LDClientInterface client;
+ private final LDLogger logger;
+
+ /**
+ * Creates an AI client wrapping the given LaunchDarkly client.
+ *
+ * No assertion is made about the state of the supplied client; it is the caller's responsibility
+ * to ensure it is properly configured and initialized before relying on AI Config functionality.
+ *
+ * Construction emits a single {@code $ld:ai:sdk:info} event identifying this AI SDK and version.
+ *
+ * @param client a fully-configured LaunchDarkly client
+ */
+ public LDAIClient(LDClientInterface client) {
+ this.client = client;
+ this.logger = client.getLogger() != null ? client.getLogger() : LDLogger.none();
+
+ client.trackMetric(
+ TRACK_SDK_INFO,
+ INIT_TRACK_CONTEXT,
+ LDValue.buildObject()
+ .put("aiSdkName", AISdkInfo.AI_SDK_NAME)
+ .put("aiSdkVersion", AISdkInfo.AI_SDK_VERSION)
+ .put("aiSdkLanguage", AISdkInfo.AI_SDK_LANGUAGE)
+ .build(),
+ 1);
+ }
+
+ /**
+ * Retrieves a completion ("traditional" chat-style) AI Config.
+ *
+ * @param key the configuration key
+ * @param context the evaluation context
+ * @param defaultValue the default value used when no flag variation is available; when
+ * {@code null}, a disabled default is used
+ * @param variables variables for message interpolation, or {@code null}
+ * @return the completion config, with a tracker factory for gathering metrics
+ */
+ public AICompletionConfig completionConfig(
+ String key,
+ LDContext context,
+ AICompletionConfigDefault defaultValue,
+ Map variables) {
+ client.trackMetric(TRACK_USAGE_COMPLETION_CONFIG, context, LDValue.of(key), 1);
+
+ AICompletionConfigDefault effectiveDefault =
+ defaultValue != null ? defaultValue : AICompletionConfigDefault.disabled();
+ Evaluation evaluation = evaluate(key, context, effectiveDefault.toLDValue(), variables, null);
+
+ return AICompletionConfig.builder(key)
+ .enabled(evaluation.enabled)
+ .model(evaluation.model)
+ .provider(evaluation.provider)
+ .messages(evaluation.messages)
+ .judgeConfiguration(evaluation.judgeConfiguration)
+ .tools(resolveTools(evaluation.variation))
+ .trackerFactory(evaluation.trackerFactory)
+ .evaluator(Evaluator.noop())
+ .build();
+ }
+
+ /**
+ * Retrieves a single AI Config agent.
+ *
+ * @param key the agent configuration key
+ * @param context the evaluation context
+ * @param defaultValue the default value used when no flag variation is available; when
+ * {@code null}, a disabled default is used
+ * @param variables variables for instruction interpolation, or {@code null}
+ * @return the agent config, with a tracker factory for gathering metrics
+ */
+ public AIAgentConfig agentConfig(
+ String key,
+ LDContext context,
+ AIAgentConfigDefault defaultValue,
+ Map variables) {
+ client.trackMetric(TRACK_USAGE_AGENT_CONFIG, context, LDValue.of(key), 1);
+ return evaluateAgent(key, context, defaultValue != null ? defaultValue : AIAgentConfigDefault.disabled(),
+ variables, null);
+ }
+
+ /**
+ * Retrieves multiple AI Config agents in a single call.
+ *
+ * @param requests the agent requests, each with its own key, default, and variables
+ * @param context the evaluation context
+ * @return a map from agent key to its resolved {@link AIAgentConfig}
+ */
+ public Map agentConfigs(List requests, LDContext context) {
+ int agentCount = requests.size();
+ client.trackMetric(TRACK_USAGE_AGENT_CONFIGS, context, LDValue.of(agentCount), agentCount);
+
+ Map result = new LinkedHashMap<>();
+ for (AIAgentConfigRequest request : requests) {
+ AIAgentConfigDefault requestDefault =
+ request.getDefaultValue() != null ? request.getDefaultValue() : AIAgentConfigDefault.disabled();
+ AIAgentConfig agent = evaluateAgent(request.getKey(), context, requestDefault, request.getVariables(), null);
+ result.put(request.getKey(), agent);
+ }
+ return result;
+ }
+
+ /**
+ * Retrieves a judge AI Config used to evaluate AI outputs.
+ *
+ * @param key the judge configuration key
+ * @param context the evaluation context
+ * @param defaultValue the default value used when no flag variation is available; when
+ * {@code null}, a disabled default is used
+ * @param variables variables for message interpolation, or {@code null}
+ * @return the judge config, with a tracker factory for gathering metrics
+ */
+ public AIJudgeConfig judgeConfig(
+ String key,
+ LDContext context,
+ AIJudgeConfigDefault defaultValue,
+ Map variables) {
+ client.trackMetric(TRACK_USAGE_JUDGE_CONFIG, context, LDValue.of(key), 1);
+
+ AIJudgeConfigDefault effectiveDefault =
+ defaultValue != null ? defaultValue : AIJudgeConfigDefault.disabled();
+ Evaluation evaluation = evaluate(key, context, effectiveDefault.toLDValue(), variables, null);
+
+ String evaluationMetricKey = extractEvaluationMetricKey(evaluation.variation);
+
+ return AIJudgeConfig.builder(key)
+ .enabled(evaluation.enabled)
+ .model(evaluation.model)
+ .provider(evaluation.provider)
+ .messages(evaluation.messages)
+ .evaluationMetricKey(evaluationMetricKey)
+ .trackerFactory(evaluation.trackerFactory)
+ .build();
+ }
+
+ /**
+ * Reconstructs an {@link AIConfigTracker} from a resumption token previously produced by
+ * {@link AIConfigTracker#getResumptionToken()}.
+ *
+ * Instances are produced by {@code LDAIClient.agentConfig} and {@code LDAIClient.agentConfigs};
+ * application code does not normally construct them directly.
+ */
+public final class AIAgentConfig extends AIConfig {
+ private final String instructions;
+ private final JudgeConfiguration judgeConfiguration;
+ private final Map tools;
+ private final Evaluator evaluator;
+
+ private AIAgentConfig(Builder builder) {
+ super(builder.key, builder.enabled, builder.model, builder.provider, builder.trackerFactory);
+ this.instructions = builder.instructions;
+ this.judgeConfiguration = builder.judgeConfiguration;
+ this.tools = builder.tools == null ? null : Collections.unmodifiableMap(builder.tools);
+ this.evaluator = builder.evaluator == null ? Evaluator.noop() : builder.evaluator;
+ }
+
+ /**
+ * Returns the agent instructions, already interpolated.
+ *
+ * @return the instructions, or {@code null} if none were provided
+ */
+ public String getInstructions() {
+ return instructions;
+ }
+
+ /**
+ * Returns the judge configuration attached to this config.
+ *
+ * @return the judge configuration, or {@code null} if none was provided
+ */
+ public JudgeConfiguration getJudgeConfiguration() {
+ return judgeConfiguration;
+ }
+
+ /**
+ * Returns the root-level tools map.
+ *
+ * @return an unmodifiable map of tool name to tool, or {@code null} if no tools were provided
+ */
+ public Map getTools() {
+ return tools;
+ }
+
+ /**
+ * Returns the evaluator built from this config's judge configuration.
+ *
+ * @return the evaluator (never {@code null})
+ */
+ public Evaluator getEvaluator() {
+ return evaluator;
+ }
+
+ /**
+ * Creates a builder for an {@link AIAgentConfig}.
+ *
+ * @param key the configuration key
+ * @return a new builder
+ */
+ public static Builder builder(String key) {
+ return new Builder(key);
+ }
+
+ /**
+ * A builder for {@link AIAgentConfig} instances.
+ */
+ public static final class Builder {
+ private final String key;
+ private boolean enabled;
+ private ModelConfig model;
+ private ProviderConfig provider;
+ private Supplier trackerFactory;
+ private String instructions;
+ private JudgeConfiguration judgeConfiguration;
+ private Map tools;
+ private Evaluator evaluator;
+
+ private Builder(String key) {
+ this.key = key;
+ }
+
+ /** @param enabled whether the config is enabled @return this builder */
+ public Builder enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /** @param model the model configuration @return this builder */
+ public Builder model(ModelConfig model) {
+ this.model = model;
+ return this;
+ }
+
+ /** @param provider the provider configuration @return this builder */
+ public Builder provider(ProviderConfig provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ /** @param trackerFactory the per-invocation tracker factory @return this builder */
+ public Builder trackerFactory(Supplier trackerFactory) {
+ this.trackerFactory = trackerFactory;
+ return this;
+ }
+
+ /** @param instructions the interpolated agent instructions @return this builder */
+ public Builder instructions(String instructions) {
+ this.instructions = instructions;
+ return this;
+ }
+
+ /** @param judgeConfiguration the judge configuration @return this builder */
+ public Builder judgeConfiguration(JudgeConfiguration judgeConfiguration) {
+ this.judgeConfiguration = judgeConfiguration;
+ return this;
+ }
+
+ /** @param tools the root-level tools map @return this builder */
+ public Builder tools(Map tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ /** @param evaluator the evaluator built from the judge configuration @return this builder */
+ public Builder evaluator(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ return this;
+ }
+
+ /**
+ * Builds the agent config.
+ *
+ * @return a new {@link AIAgentConfig}
+ */
+ public AIAgentConfig build() {
+ return new AIAgentConfig(this);
+ }
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentConfigDefault.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentConfigDefault.java
new file mode 100644
index 00000000..90675120
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentConfigDefault.java
@@ -0,0 +1,143 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.ObjectBuilder;
+
+import java.util.Map;
+
+/**
+ * A user-constructed default value for {@code LDAIClient.agentConfig} and
+ * {@code LDAIClient.agentConfigs}.
+ */
+public final class AIAgentConfigDefault extends AIConfigDefault {
+ private final String instructions;
+ private final JudgeConfiguration judgeConfiguration;
+ private final Map tools;
+
+ private AIAgentConfigDefault(Builder builder) {
+ super(builder.enabled, builder.model, builder.provider);
+ this.instructions = builder.instructions;
+ this.judgeConfiguration = builder.judgeConfiguration;
+ this.tools = builder.tools;
+ }
+
+ /**
+ * Returns a disabled default.
+ *
+ * @return a default with {@code enabled} set to {@code false}
+ */
+ public static AIAgentConfigDefault disabled() {
+ return builder().enabled(false).build();
+ }
+
+ /**
+ * Returns the default agent instructions.
+ *
+ * @return the instructions, or {@code null} if none were provided
+ */
+ public String getInstructions() {
+ return instructions;
+ }
+
+ /**
+ * Returns the default judge configuration.
+ *
+ * @return the judge configuration, or {@code null} if none was provided
+ */
+ public JudgeConfiguration getJudgeConfiguration() {
+ return judgeConfiguration;
+ }
+
+ /**
+ * Returns the default tools map.
+ *
+ * @return the tools, or {@code null} if none were provided
+ */
+ public Map getTools() {
+ return tools;
+ }
+
+ @Override
+ public LDValue toLDValue() {
+ ObjectBuilder builder = baseObject();
+ if (instructions != null) {
+ builder.put("instructions", instructions);
+ }
+ if (judgeConfiguration != null) {
+ builder.put("judgeConfiguration", judgeConfiguration.toLDValue());
+ }
+ if (tools != null) {
+ builder.put("tools", AICompletionConfigDefault.toolsToLDValue(tools));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Creates a builder for an {@link AIAgentConfigDefault}.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A builder for {@link AIAgentConfigDefault} instances.
+ */
+ public static final class Builder {
+ private Boolean enabled;
+ private ModelConfig model;
+ private ProviderConfig provider;
+ private String instructions;
+ private JudgeConfiguration judgeConfiguration;
+ private Map tools;
+
+ private Builder() {
+ }
+
+ /** @param enabled whether the config should be considered enabled @return this builder */
+ public Builder enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /** @param model the model configuration @return this builder */
+ public Builder model(ModelConfig model) {
+ this.model = model;
+ return this;
+ }
+
+ /** @param provider the provider configuration @return this builder */
+ public Builder provider(ProviderConfig provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ /** @param instructions the default agent instructions @return this builder */
+ public Builder instructions(String instructions) {
+ this.instructions = instructions;
+ return this;
+ }
+
+ /** @param judgeConfiguration the default judge configuration @return this builder */
+ public Builder judgeConfiguration(JudgeConfiguration judgeConfiguration) {
+ this.judgeConfiguration = judgeConfiguration;
+ return this;
+ }
+
+ /** @param tools the default tools map @return this builder */
+ public Builder tools(Map tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ /**
+ * Builds the default.
+ *
+ * @return a new {@link AIAgentConfigDefault}
+ */
+ public AIAgentConfigDefault build() {
+ return new AIAgentConfigDefault(this);
+ }
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentConfigRequest.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentConfigRequest.java
new file mode 100644
index 00000000..cb845300
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentConfigRequest.java
@@ -0,0 +1,66 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A single request entry passed to {@code LDAIClient.agentConfigs}, combining an agent key with its
+ * own default configuration and interpolation variables.
+ */
+public final class AIAgentConfigRequest {
+ private final String key;
+ private final AIAgentConfigDefault defaultValue;
+ private final Map variables;
+
+ /**
+ * Creates a request for an agent with no default and no variables.
+ *
+ * @param key the agent configuration key
+ */
+ public AIAgentConfigRequest(String key) {
+ this(key, null, null);
+ }
+
+ /**
+ * Creates a request for an agent.
+ *
+ * @param key the agent configuration key
+ * @param defaultValue the default value to use when no flag variation is available, or {@code null}
+ * @param variables the variables for instruction interpolation, or {@code null}
+ */
+ public AIAgentConfigRequest(String key, AIAgentConfigDefault defaultValue, Map variables) {
+ this.key = key;
+ this.defaultValue = defaultValue;
+ this.variables = variables == null
+ ? null
+ : Collections.unmodifiableMap(new HashMap<>(variables));
+ }
+
+ /**
+ * Returns the agent configuration key.
+ *
+ * @return the key
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the default value for this agent.
+ *
+ * @return the default, or {@code null} if none was provided
+ */
+ public AIAgentConfigDefault getDefaultValue() {
+ return defaultValue;
+ }
+
+ /**
+ * Returns the interpolation variables for this agent.
+ *
+ * @return an unmodifiable map of variables, or {@code null} if none were provided
+ */
+ public Map getVariables() {
+ return variables;
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentGraphConfig.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentGraphConfig.java
new file mode 100644
index 00000000..44fc064f
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIAgentGraphConfig.java
@@ -0,0 +1,67 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Configuration describing an agentic graph flow composed of multiple interconnected
+ * {@link AIAgentConfig} nodes.
+ */
+public final class AIAgentGraphConfig {
+ private final String key;
+ private final String rootConfigKey;
+ private final List edges;
+ private final boolean enabled;
+
+ /**
+ * Creates an agent graph configuration.
+ *
+ * @param key the graph configuration key
+ * @param rootConfigKey the key of the root agent node
+ * @param edges the edges defining relationships between agent nodes
+ * @param enabled whether the graph is enabled
+ */
+ public AIAgentGraphConfig(String key, String rootConfigKey, List edges, boolean enabled) {
+ this.key = key;
+ this.rootConfigKey = rootConfigKey;
+ this.edges = Collections.unmodifiableList(new ArrayList<>(edges));
+ this.enabled = enabled;
+ }
+
+ /**
+ * Returns the graph configuration key.
+ *
+ * @return the key
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the key of the root agent node.
+ *
+ * @return the root config key
+ */
+ public String getRootConfigKey() {
+ return rootConfigKey;
+ }
+
+ /**
+ * Returns the edges defining relationships between agent nodes.
+ *
+ * @return an unmodifiable list of edges
+ */
+ public List getEdges() {
+ return edges;
+ }
+
+ /**
+ * Returns whether the graph is enabled.
+ *
+ * @return {@code true} if enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AICompletionConfig.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AICompletionConfig.java
new file mode 100644
index 00000000..0f0fce73
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AICompletionConfig.java
@@ -0,0 +1,154 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.server.ai.evaluation.Evaluator;
+import com.launchdarkly.sdk.server.ai.tracking.AIConfigTracker;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * A traditional ("completion" mode) AI Config, composed of chat-style messages.
+ *
+ * Instances are produced by {@code LDAIClient.completionConfig}; application code does not normally
+ * construct them directly.
+ */
+public final class AICompletionConfig extends AIConfig {
+ private final List messages;
+ private final JudgeConfiguration judgeConfiguration;
+ private final Map tools;
+ private final Evaluator evaluator;
+
+ private AICompletionConfig(Builder builder) {
+ super(builder.key, builder.enabled, builder.model, builder.provider, builder.trackerFactory);
+ this.messages = builder.messages == null ? null : Collections.unmodifiableList(builder.messages);
+ this.judgeConfiguration = builder.judgeConfiguration;
+ this.tools = builder.tools == null ? null : Collections.unmodifiableMap(builder.tools);
+ this.evaluator = builder.evaluator == null ? Evaluator.noop() : builder.evaluator;
+ }
+
+ /**
+ * Returns the prompt messages, with their content already interpolated.
+ *
+ * @return the messages, or {@code null} if none were provided
+ */
+ public List getMessages() {
+ return messages;
+ }
+
+ /**
+ * Returns the judge configuration attached to this config.
+ *
+ * @return the judge configuration, or {@code null} if none was provided
+ */
+ public JudgeConfiguration getJudgeConfiguration() {
+ return judgeConfiguration;
+ }
+
+ /**
+ * Returns the root-level tools map.
+ *
+ * @return an unmodifiable map of tool name to tool, or {@code null} if no tools were provided
+ */
+ public Map getTools() {
+ return tools;
+ }
+
+ /**
+ * Returns the evaluator built from this config's judge configuration.
+ *
+ * When no judges are configured, this is a no-op evaluator that produces an empty result.
+ *
+ * @return the evaluator (never {@code null})
+ */
+ public Evaluator getEvaluator() {
+ return evaluator;
+ }
+
+ /**
+ * Creates a builder for an {@link AICompletionConfig}.
+ *
+ * @param key the configuration key
+ * @return a new builder
+ */
+ public static Builder builder(String key) {
+ return new Builder(key);
+ }
+
+ /**
+ * A builder for {@link AICompletionConfig} instances.
+ */
+ public static final class Builder {
+ private final String key;
+ private boolean enabled;
+ private ModelConfig model;
+ private ProviderConfig provider;
+ private Supplier trackerFactory;
+ private List messages;
+ private JudgeConfiguration judgeConfiguration;
+ private Map tools;
+ private Evaluator evaluator;
+
+ private Builder(String key) {
+ this.key = key;
+ }
+
+ /** @param enabled whether the config is enabled @return this builder */
+ public Builder enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /** @param model the model configuration @return this builder */
+ public Builder model(ModelConfig model) {
+ this.model = model;
+ return this;
+ }
+
+ /** @param provider the provider configuration @return this builder */
+ public Builder provider(ProviderConfig provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ /** @param trackerFactory the per-invocation tracker factory @return this builder */
+ public Builder trackerFactory(Supplier trackerFactory) {
+ this.trackerFactory = trackerFactory;
+ return this;
+ }
+
+ /** @param messages the interpolated prompt messages @return this builder */
+ public Builder messages(List messages) {
+ this.messages = messages;
+ return this;
+ }
+
+ /** @param judgeConfiguration the judge configuration @return this builder */
+ public Builder judgeConfiguration(JudgeConfiguration judgeConfiguration) {
+ this.judgeConfiguration = judgeConfiguration;
+ return this;
+ }
+
+ /** @param tools the root-level tools map @return this builder */
+ public Builder tools(Map tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ /** @param evaluator the evaluator built from the judge configuration @return this builder */
+ public Builder evaluator(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ return this;
+ }
+
+ /**
+ * Builds the completion config.
+ *
+ * @return a new {@link AICompletionConfig}
+ */
+ public AICompletionConfig build() {
+ return new AICompletionConfig(this);
+ }
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AICompletionConfigDefault.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AICompletionConfigDefault.java
new file mode 100644
index 00000000..384eafc9
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AICompletionConfigDefault.java
@@ -0,0 +1,162 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.ArrayBuilder;
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.ObjectBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A user-constructed default value for {@code LDAIClient.completionConfig}.
+ */
+public final class AICompletionConfigDefault extends AIConfigDefault {
+ private final List messages;
+ private final JudgeConfiguration judgeConfiguration;
+ private final Map tools;
+
+ private AICompletionConfigDefault(Builder builder) {
+ super(builder.enabled, builder.model, builder.provider);
+ this.messages = builder.messages;
+ this.judgeConfiguration = builder.judgeConfiguration;
+ this.tools = builder.tools;
+ }
+
+ /**
+ * Returns a disabled default.
+ *
+ * @return a default with {@code enabled} set to {@code false}
+ */
+ public static AICompletionConfigDefault disabled() {
+ return builder().enabled(false).build();
+ }
+
+ /**
+ * Returns the default prompt messages.
+ *
+ * @return the messages, or {@code null} if none were provided
+ */
+ public List getMessages() {
+ return messages;
+ }
+
+ /**
+ * Returns the default judge configuration.
+ *
+ * @return the judge configuration, or {@code null} if none was provided
+ */
+ public JudgeConfiguration getJudgeConfiguration() {
+ return judgeConfiguration;
+ }
+
+ /**
+ * Returns the default tools map.
+ *
+ * @return the tools, or {@code null} if none were provided
+ */
+ public Map getTools() {
+ return tools;
+ }
+
+ @Override
+ public LDValue toLDValue() {
+ ObjectBuilder builder = baseObject();
+ builder.put("messages", messagesToLDValue(messages));
+ if (judgeConfiguration != null) {
+ builder.put("judgeConfiguration", judgeConfiguration.toLDValue());
+ }
+ if (tools != null) {
+ builder.put("tools", toolsToLDValue(tools));
+ }
+ return builder.build();
+ }
+
+ static LDValue messagesToLDValue(List messages) {
+ if (messages == null) {
+ return LDValue.ofNull();
+ }
+ ArrayBuilder array = LDValue.buildArray();
+ for (LDMessage message : messages) {
+ array.add(message.toLDValue());
+ }
+ return array.build();
+ }
+
+ static LDValue toolsToLDValue(Map tools) {
+ ObjectBuilder object = LDValue.buildObject();
+ for (Map.Entry entry : tools.entrySet()) {
+ object.put(entry.getKey(), entry.getValue().toLDValue());
+ }
+ return object.build();
+ }
+
+ /**
+ * Creates a builder for an {@link AICompletionConfigDefault}.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A builder for {@link AICompletionConfigDefault} instances.
+ */
+ public static final class Builder {
+ private Boolean enabled;
+ private ModelConfig model;
+ private ProviderConfig provider;
+ private List messages;
+ private JudgeConfiguration judgeConfiguration;
+ private Map tools;
+
+ private Builder() {
+ }
+
+ /** @param enabled whether the config should be considered enabled @return this builder */
+ public Builder enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /** @param model the model configuration @return this builder */
+ public Builder model(ModelConfig model) {
+ this.model = model;
+ return this;
+ }
+
+ /** @param provider the provider configuration @return this builder */
+ public Builder provider(ProviderConfig provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ /** @param messages the default prompt messages @return this builder */
+ public Builder messages(List messages) {
+ this.messages = messages == null ? null : new ArrayList<>(messages);
+ return this;
+ }
+
+ /** @param judgeConfiguration the default judge configuration @return this builder */
+ public Builder judgeConfiguration(JudgeConfiguration judgeConfiguration) {
+ this.judgeConfiguration = judgeConfiguration;
+ return this;
+ }
+
+ /** @param tools the default tools map @return this builder */
+ public Builder tools(Map tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ /**
+ * Builds the default.
+ *
+ * @return a new {@link AICompletionConfigDefault}
+ */
+ public AICompletionConfigDefault build() {
+ return new AICompletionConfigDefault(this);
+ }
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIConfig.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIConfig.java
new file mode 100644
index 00000000..db2a9cca
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIConfig.java
@@ -0,0 +1,90 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.server.ai.tracking.AIConfigTracker;
+
+import java.util.function.Supplier;
+
+/**
+ * Base type for the AI Config variants returned by {@code LDAIClient}.
+ *
+ * All AI Config types share a key, an enabled flag, optional model and provider configuration, and
+ * the ability to mint a fresh {@link AIConfigTracker} for an AI run via {@link #createTracker()}.
+ */
+public abstract class AIConfig {
+ private final String key;
+ private final boolean enabled;
+ private final ModelConfig model;
+ private final ProviderConfig provider;
+ private final Supplier trackerFactory;
+
+ /**
+ * Constructs a base AI Config.
+ *
+ * @param key the configuration key
+ * @param enabled whether the configuration is enabled
+ * @param model the model configuration, or {@code null}
+ * @param provider the provider configuration, or {@code null}
+ * @param trackerFactory a factory that produces a fresh tracker per invocation
+ */
+ protected AIConfig(
+ String key,
+ boolean enabled,
+ ModelConfig model,
+ ProviderConfig provider,
+ Supplier trackerFactory) {
+ this.key = key;
+ this.enabled = enabled;
+ this.model = model;
+ this.provider = provider;
+ this.trackerFactory = trackerFactory;
+ }
+
+ /**
+ * Returns the configuration key used for tracking and identification.
+ *
+ * @return the key
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns whether the configuration is enabled.
+ *
+ * @return {@code true} if enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Returns the model configuration.
+ *
+ * @return the model configuration, or {@code null} if none was provided
+ */
+ public ModelConfig getModel() {
+ return model;
+ }
+
+ /**
+ * Returns the provider configuration.
+ *
+ * @return the provider configuration, or {@code null} if none was provided
+ */
+ public ProviderConfig getProvider() {
+ return provider;
+ }
+
+ /**
+ * Creates a new {@link AIConfigTracker} for a single AI run.
+ *
+ * Each call mints a tracker with a new {@code runId} (a UUIDv4) so that LaunchDarkly can correlate
+ * the run's events in metrics views. Call this once per AI run; metrics from different runs cannot
+ * be combined.
+ *
+ * @return a fresh tracker
+ */
+ public AIConfigTracker createTracker() {
+ return trackerFactory.get();
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIConfigDefault.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIConfigDefault.java
new file mode 100644
index 00000000..11618a83
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIConfigDefault.java
@@ -0,0 +1,77 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.ObjectBuilder;
+
+/**
+ * Base type for the user-constructed default values passed to the {@code LDAIClient} configuration
+ * methods. A default is used as the fallback value when no flag variation is available.
+ */
+public abstract class AIConfigDefault {
+ private final Boolean enabled;
+ private final ModelConfig model;
+ private final ProviderConfig provider;
+
+ /**
+ * Constructs a base default.
+ *
+ * @param enabled whether the config should be considered enabled, or {@code null} to leave unset
+ * @param model the model configuration, or {@code null}
+ * @param provider the provider configuration, or {@code null}
+ */
+ protected AIConfigDefault(Boolean enabled, ModelConfig model, ProviderConfig provider) {
+ this.enabled = enabled;
+ this.model = model;
+ this.provider = provider;
+ }
+
+ /**
+ * Returns whether the config should be considered enabled.
+ *
+ * @return the enabled flag, or {@code null} if unset
+ */
+ public Boolean getEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Returns the model configuration.
+ *
+ * @return the model configuration, or {@code null}
+ */
+ public ModelConfig getModel() {
+ return model;
+ }
+
+ /**
+ * Returns the provider configuration.
+ *
+ * @return the provider configuration, or {@code null}
+ */
+ public ProviderConfig getProvider() {
+ return provider;
+ }
+
+ /**
+ * Builds an {@link LDValue} object containing the fields common to all default types, suitable for
+ * use as the default value of a JSON flag evaluation.
+ *
+ * @return an object builder seeded with {@code _ldMeta}, {@code model}, and {@code provider}
+ */
+ protected ObjectBuilder baseObject() {
+ LDValue ldMeta = LDValue.buildObject()
+ .put("enabled", enabled != null && enabled)
+ .build();
+ return LDValue.buildObject()
+ .put("_ldMeta", ldMeta)
+ .put("model", model == null ? LDValue.ofNull() : model.toLDValue())
+ .put("provider", provider == null ? LDValue.ofNull() : provider.toLDValue());
+ }
+
+ /**
+ * Renders this default value as an {@link LDValue} object.
+ *
+ * @return the JSON representation
+ */
+ public abstract LDValue toLDValue();
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIJudgeConfig.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIJudgeConfig.java
new file mode 100644
index 00000000..b9332fe6
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIJudgeConfig.java
@@ -0,0 +1,115 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.server.ai.tracking.AIConfigTracker;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A judge AI Config ("judge" mode), used to evaluate AI outputs. It is composed of chat-style
+ * evaluation messages and a required evaluation metric key.
+ *
+ * Instances are produced by {@code LDAIClient.judgeConfig}; application code does not normally
+ * construct them directly.
+ */
+public final class AIJudgeConfig extends AIConfig {
+ private final List messages;
+ private final String evaluationMetricKey;
+
+ private AIJudgeConfig(Builder builder) {
+ super(builder.key, builder.enabled, builder.model, builder.provider, builder.trackerFactory);
+ this.messages = builder.messages == null ? null : Collections.unmodifiableList(builder.messages);
+ this.evaluationMetricKey = builder.evaluationMetricKey;
+ }
+
+ /**
+ * Returns the evaluation prompt messages, with their content already interpolated.
+ *
+ * @return the messages, or {@code null} if none were provided
+ */
+ public List getMessages() {
+ return messages;
+ }
+
+ /**
+ * Returns the metric key that this judge evaluates.
+ *
+ * @return the evaluation metric key, or {@code null} if none was provided
+ */
+ public String getEvaluationMetricKey() {
+ return evaluationMetricKey;
+ }
+
+ /**
+ * Creates a builder for an {@link AIJudgeConfig}.
+ *
+ * @param key the configuration key
+ * @return a new builder
+ */
+ public static Builder builder(String key) {
+ return new Builder(key);
+ }
+
+ /**
+ * A builder for {@link AIJudgeConfig} instances.
+ */
+ public static final class Builder {
+ private final String key;
+ private boolean enabled;
+ private ModelConfig model;
+ private ProviderConfig provider;
+ private Supplier trackerFactory;
+ private List messages;
+ private String evaluationMetricKey;
+
+ private Builder(String key) {
+ this.key = key;
+ }
+
+ /** @param enabled whether the config is enabled @return this builder */
+ public Builder enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /** @param model the model configuration @return this builder */
+ public Builder model(ModelConfig model) {
+ this.model = model;
+ return this;
+ }
+
+ /** @param provider the provider configuration @return this builder */
+ public Builder provider(ProviderConfig provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ /** @param trackerFactory the per-invocation tracker factory @return this builder */
+ public Builder trackerFactory(Supplier trackerFactory) {
+ this.trackerFactory = trackerFactory;
+ return this;
+ }
+
+ /** @param messages the interpolated evaluation messages @return this builder */
+ public Builder messages(List messages) {
+ this.messages = messages;
+ return this;
+ }
+
+ /** @param evaluationMetricKey the metric key this judge evaluates @return this builder */
+ public Builder evaluationMetricKey(String evaluationMetricKey) {
+ this.evaluationMetricKey = evaluationMetricKey;
+ return this;
+ }
+
+ /**
+ * Builds the judge config.
+ *
+ * @return a new {@link AIJudgeConfig}
+ */
+ public AIJudgeConfig build() {
+ return new AIJudgeConfig(this);
+ }
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIJudgeConfigDefault.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIJudgeConfigDefault.java
new file mode 100644
index 00000000..b1dd6094
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AIJudgeConfigDefault.java
@@ -0,0 +1,118 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.ObjectBuilder;
+
+import java.util.List;
+
+/**
+ * A user-constructed default value for {@code LDAIClient.judgeConfig}.
+ */
+public final class AIJudgeConfigDefault extends AIConfigDefault {
+ private final List messages;
+ private final String evaluationMetricKey;
+
+ private AIJudgeConfigDefault(Builder builder) {
+ super(builder.enabled, builder.model, builder.provider);
+ this.messages = builder.messages;
+ this.evaluationMetricKey = builder.evaluationMetricKey;
+ }
+
+ /**
+ * Returns a disabled default.
+ *
+ * @return a default with {@code enabled} set to {@code false}
+ */
+ public static AIJudgeConfigDefault disabled() {
+ return builder().enabled(false).build();
+ }
+
+ /**
+ * Returns the default evaluation messages.
+ *
+ * @return the messages, or {@code null} if none were provided
+ */
+ public List getMessages() {
+ return messages;
+ }
+
+ /**
+ * Returns the default evaluation metric key.
+ *
+ * @return the evaluation metric key, or {@code null} if none was provided
+ */
+ public String getEvaluationMetricKey() {
+ return evaluationMetricKey;
+ }
+
+ @Override
+ public LDValue toLDValue() {
+ ObjectBuilder builder = baseObject();
+ builder.put("messages", AICompletionConfigDefault.messagesToLDValue(messages));
+ builder.put("evaluationMetricKey",
+ evaluationMetricKey == null ? LDValue.ofNull() : LDValue.of(evaluationMetricKey));
+ return builder.build();
+ }
+
+ /**
+ * Creates a builder for an {@link AIJudgeConfigDefault}.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A builder for {@link AIJudgeConfigDefault} instances.
+ */
+ public static final class Builder {
+ private Boolean enabled;
+ private ModelConfig model;
+ private ProviderConfig provider;
+ private List messages;
+ private String evaluationMetricKey;
+
+ private Builder() {
+ }
+
+ /** @param enabled whether the config should be considered enabled @return this builder */
+ public Builder enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /** @param model the model configuration @return this builder */
+ public Builder model(ModelConfig model) {
+ this.model = model;
+ return this;
+ }
+
+ /** @param provider the provider configuration @return this builder */
+ public Builder provider(ProviderConfig provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ /** @param messages the default evaluation messages @return this builder */
+ public Builder messages(List messages) {
+ this.messages = messages;
+ return this;
+ }
+
+ /** @param evaluationMetricKey the metric key this judge evaluates @return this builder */
+ public Builder evaluationMetricKey(String evaluationMetricKey) {
+ this.evaluationMetricKey = evaluationMetricKey;
+ return this;
+ }
+
+ /**
+ * Builds the default.
+ *
+ * @return a new {@link AIJudgeConfigDefault}
+ */
+ public AIJudgeConfigDefault build() {
+ return new AIJudgeConfigDefault(this);
+ }
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Edge.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Edge.java
new file mode 100644
index 00000000..8e5873a4
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Edge.java
@@ -0,0 +1,92 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+
+import java.util.Objects;
+
+/**
+ * An edge in an {@link AIAgentGraphConfig}, describing a directed relationship between two agent
+ * nodes and any handoff options associated with it.
+ */
+public final class Edge {
+ private final String key;
+ private final String sourceConfig;
+ private final String targetConfig;
+ private final LDValue handoff;
+
+ /**
+ * Creates an edge.
+ *
+ * @param key the edge key
+ * @param sourceConfig the key of the source agent node
+ * @param targetConfig the key of the target agent node
+ * @param handoff handoff options for this relationship, or {@link LDValue#ofNull()}
+ */
+ public Edge(String key, String sourceConfig, String targetConfig, LDValue handoff) {
+ this.key = key;
+ this.sourceConfig = sourceConfig;
+ this.targetConfig = targetConfig;
+ this.handoff = handoff == null ? LDValue.ofNull() : handoff;
+ }
+
+ /**
+ * Returns the edge key.
+ *
+ * @return the key
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the key of the source agent node.
+ *
+ * @return the source config key
+ */
+ public String getSourceConfig() {
+ return sourceConfig;
+ }
+
+ /**
+ * Returns the key of the target agent node.
+ *
+ * @return the target config key
+ */
+ public String getTargetConfig() {
+ return targetConfig;
+ }
+
+ /**
+ * Returns the handoff options for this relationship.
+ *
+ * @return the handoff options, or {@link LDValue#ofNull()} if none
+ */
+ public LDValue getHandoff() {
+ return handoff;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Edge)) {
+ return false;
+ }
+ Edge other = (Edge) o;
+ return Objects.equals(key, other.key)
+ && Objects.equals(sourceConfig, other.sourceConfig)
+ && Objects.equals(targetConfig, other.targetConfig)
+ && Objects.equals(handoff, other.handoff);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, sourceConfig, targetConfig, handoff);
+ }
+
+ @Override
+ public String toString() {
+ return "Edge{key=" + key + ", sourceConfig=" + sourceConfig + ", targetConfig=" + targetConfig + "}";
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/JudgeConfiguration.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/JudgeConfiguration.java
new file mode 100644
index 00000000..2ae789c0
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/JudgeConfiguration.java
@@ -0,0 +1,142 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.ArrayBuilder;
+import com.launchdarkly.sdk.LDValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Configuration describing the judges attached to an AI Config for automatic online evaluation.
+ *
+ * Each entry pairs a judge configuration key with the sampling rate at which that judge should be
+ * invoked.
+ */
+public final class JudgeConfiguration {
+ /**
+ * Configuration for a single judge attachment.
+ */
+ public static final class Judge {
+ private final String key;
+ private final double samplingRate;
+
+ /**
+ * Creates a judge attachment.
+ *
+ * @param key the judge configuration key
+ * @param samplingRate the sampling rate, between {@code 0.0} and {@code 1.0}
+ */
+ public Judge(String key, double samplingRate) {
+ this.key = key;
+ this.samplingRate = samplingRate;
+ }
+
+ /**
+ * Returns the judge configuration key.
+ *
+ * @return the key
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the sampling rate.
+ *
+ * @return the sampling rate
+ */
+ public double getSamplingRate() {
+ return samplingRate;
+ }
+
+ /**
+ * Renders this judge attachment as an {@link LDValue} object.
+ *
+ * @return the JSON representation
+ */
+ public LDValue toLDValue() {
+ return LDValue.buildObject()
+ .put("key", key)
+ .put("samplingRate", samplingRate)
+ .build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Judge)) {
+ return false;
+ }
+ Judge other = (Judge) o;
+ return Double.compare(samplingRate, other.samplingRate) == 0 && Objects.equals(key, other.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, samplingRate);
+ }
+
+ @Override
+ public String toString() {
+ return "Judge{key=" + key + ", samplingRate=" + samplingRate + "}";
+ }
+ }
+
+ private final List judges;
+
+ /**
+ * Creates a judge configuration.
+ *
+ * @param judges the judges to attach
+ */
+ public JudgeConfiguration(List judges) {
+ this.judges = Collections.unmodifiableList(new ArrayList<>(judges));
+ }
+
+ /**
+ * Returns the judges in this configuration.
+ *
+ * @return an unmodifiable list of judges
+ */
+ public List getJudges() {
+ return judges;
+ }
+
+ /**
+ * Renders this judge configuration as an {@link LDValue} object.
+ *
+ * @return the JSON representation
+ */
+ public LDValue toLDValue() {
+ ArrayBuilder array = LDValue.buildArray();
+ for (Judge judge : judges) {
+ array.add(judge.toLDValue());
+ }
+ return LDValue.buildObject().put("judges", array.build()).build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof JudgeConfiguration)) {
+ return false;
+ }
+ return Objects.equals(judges, ((JudgeConfiguration) o).judges);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(judges);
+ }
+
+ @Override
+ public String toString() {
+ return "JudgeConfiguration{judges=" + judges + "}";
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/LDMessage.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/LDMessage.java
new file mode 100644
index 00000000..9d477182
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/LDMessage.java
@@ -0,0 +1,118 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+
+import java.util.Objects;
+
+/**
+ * A single message used to compose a prompt for an AI Config.
+ *
+ * A message pairs a {@code role} (one of {@code system}, {@code user}, or {@code assistant}) with
+ * its {@code content}. When a message is delivered as part of an AI Config, its content is
+ * interpolated using Mustache templating before the message is returned to the caller.
+ */
+public final class LDMessage {
+ /** The {@code system} role. */
+ public static final String ROLE_SYSTEM = "system";
+ /** The {@code user} role. */
+ public static final String ROLE_USER = "user";
+ /** The {@code assistant} role. */
+ public static final String ROLE_ASSISTANT = "assistant";
+
+ private final String role;
+ private final String content;
+
+ /**
+ * Creates a message.
+ *
+ * @param role the role of the message, typically one of {@link #ROLE_SYSTEM}, {@link #ROLE_USER},
+ * or {@link #ROLE_ASSISTANT}
+ * @param content the message content
+ */
+ public LDMessage(String role, String content) {
+ this.role = role;
+ this.content = content;
+ }
+
+ /**
+ * Creates a {@code system} message.
+ *
+ * @param content the message content
+ * @return the message
+ */
+ public static LDMessage system(String content) {
+ return new LDMessage(ROLE_SYSTEM, content);
+ }
+
+ /**
+ * Creates a {@code user} message.
+ *
+ * @param content the message content
+ * @return the message
+ */
+ public static LDMessage user(String content) {
+ return new LDMessage(ROLE_USER, content);
+ }
+
+ /**
+ * Creates an {@code assistant} message.
+ *
+ * @param content the message content
+ * @return the message
+ */
+ public static LDMessage assistant(String content) {
+ return new LDMessage(ROLE_ASSISTANT, content);
+ }
+
+ /**
+ * Returns the role of the message.
+ *
+ * @return the role
+ */
+ public String getRole() {
+ return role;
+ }
+
+ /**
+ * Returns the message content.
+ *
+ * @return the content
+ */
+ public String getContent() {
+ return content;
+ }
+
+ /**
+ * Renders this message as an {@link LDValue} object.
+ *
+ * @return the JSON representation
+ */
+ public LDValue toLDValue() {
+ return LDValue.buildObject()
+ .put("role", role)
+ .put("content", content)
+ .build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof LDMessage)) {
+ return false;
+ }
+ LDMessage other = (LDMessage) o;
+ return Objects.equals(role, other.role) && Objects.equals(content, other.content);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(role, content);
+ }
+
+ @Override
+ public String toString() {
+ return "LDMessage{role=" + role + ", content=" + content + "}";
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/LDTool.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/LDTool.java
new file mode 100644
index 00000000..e0a03291
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/LDTool.java
@@ -0,0 +1,201 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.ObjectBuilder;
+
+import java.util.Objects;
+
+/**
+ * A single tool entry from the root-level {@code tools} map of an AI Config.
+ *
+ * This is distinct from {@code model.parameters.tools[]}, which is the raw array passed to LLM
+ * providers unmodified. The root-level tools map carries additional metadata such as
+ * {@link #getCustomParameters() customParameters} that should not be forwarded to the provider.
+ */
+public final class LDTool {
+ private final String name;
+ private final String description;
+ private final String type;
+ private final LDValue parameters;
+ private final LDValue customParameters;
+
+ private LDTool(Builder builder) {
+ this.name = builder.name;
+ this.description = builder.description;
+ this.type = builder.type;
+ this.parameters = builder.parameters == null ? LDValue.ofNull() : builder.parameters;
+ this.customParameters = builder.customParameters == null ? LDValue.ofNull() : builder.customParameters;
+ }
+
+ /**
+ * Returns the tool name (which matches its key in the tools map).
+ *
+ * @return the tool name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the human-readable description of what the tool does.
+ *
+ * @return the description, or {@code null} if not specified
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Returns the tool type (for example {@code "function"}).
+ *
+ * @return the type, or {@code null} if not specified
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Returns the JSON Schema describing the tool's input parameters.
+ *
+ * @return the parameters, or {@link LDValue#ofNull()} if not specified
+ */
+ public LDValue getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Returns custom parameters that are not passed to the LLM provider.
+ *
+ * @return the custom parameters, or {@link LDValue#ofNull()} if not specified
+ */
+ public LDValue getCustomParameters() {
+ return customParameters;
+ }
+
+ /**
+ * Renders this tool as an {@link LDValue} object using the wire format (with the
+ * {@code customParameters} camelCase key).
+ *
+ * @return the JSON representation
+ */
+ public LDValue toLDValue() {
+ ObjectBuilder builder = LDValue.buildObject().put("name", name);
+ if (description != null) {
+ builder.put("description", description);
+ }
+ if (type != null) {
+ builder.put("type", type);
+ }
+ if (!parameters.isNull()) {
+ builder.put("parameters", parameters);
+ }
+ if (!customParameters.isNull()) {
+ builder.put("customParameters", customParameters);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Creates a builder for a tool with the given name.
+ *
+ * @param name the tool name
+ * @return a new builder
+ */
+ public static Builder builder(String name) {
+ return new Builder(name);
+ }
+
+ /**
+ * A builder for {@link LDTool} instances.
+ */
+ public static final class Builder {
+ private final String name;
+ private String description;
+ private String type;
+ private LDValue parameters;
+ private LDValue customParameters;
+
+ private Builder(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Sets the tool description.
+ *
+ * @param description the description
+ * @return this builder
+ */
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Sets the tool type.
+ *
+ * @param type the type
+ * @return this builder
+ */
+ public Builder type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Sets the JSON Schema describing the tool's parameters.
+ *
+ * @param parameters the parameters
+ * @return this builder
+ */
+ public Builder parameters(LDValue parameters) {
+ this.parameters = parameters;
+ return this;
+ }
+
+ /**
+ * Sets custom parameters that are not passed to the LLM provider.
+ *
+ * @param customParameters the custom parameters
+ * @return this builder
+ */
+ public Builder customParameters(LDValue customParameters) {
+ this.customParameters = customParameters;
+ return this;
+ }
+
+ /**
+ * Builds the tool.
+ *
+ * @return a new {@link LDTool}
+ */
+ public LDTool build() {
+ return new LDTool(this);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof LDTool)) {
+ return false;
+ }
+ LDTool other = (LDTool) o;
+ return Objects.equals(name, other.name)
+ && Objects.equals(description, other.description)
+ && Objects.equals(type, other.type)
+ && Objects.equals(parameters, other.parameters)
+ && Objects.equals(customParameters, other.customParameters);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, description, type, parameters, customParameters);
+ }
+
+ @Override
+ public String toString() {
+ return "LDTool{name=" + name + ", type=" + type + "}";
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/ModelConfig.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/ModelConfig.java
new file mode 100644
index 00000000..c5c354f2
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/ModelConfig.java
@@ -0,0 +1,198 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.LDValueType;
+import com.launchdarkly.sdk.ObjectBuilder;
+
+import java.util.Objects;
+
+/**
+ * Configuration describing the model associated with an AI Config, including the model name and
+ * any provider-specific parameters or custom data.
+ */
+public final class ModelConfig {
+ private final String name;
+ private final LDValue parameters;
+ private final LDValue custom;
+
+ /**
+ * Creates a model configuration with just a name.
+ *
+ * @param name the name of the model
+ */
+ public ModelConfig(String name) {
+ this(name, LDValue.ofNull(), LDValue.ofNull());
+ }
+
+ /**
+ * Creates a model configuration.
+ *
+ * @param name the name of the model
+ * @param parameters model-specific parameters as a JSON object, or {@link LDValue#ofNull()}
+ * @param custom additional customer-provided data as a JSON object, or {@link LDValue#ofNull()}
+ */
+ public ModelConfig(String name, LDValue parameters, LDValue custom) {
+ this.name = name;
+ this.parameters = parameters == null ? LDValue.ofNull() : parameters;
+ this.custom = custom == null ? LDValue.ofNull() : custom;
+ }
+
+ /**
+ * Returns the name of the model.
+ *
+ * @return the model name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Retrieves a model parameter by key.
+ *
+ * Requesting the key {@code "name"} returns the model name. Any other key is looked up in the
+ * model parameters.
+ *
+ * @param key the parameter key
+ * @return the value, or {@link LDValue#ofNull()} if not present
+ */
+ public LDValue getParameter(String key) {
+ if ("name".equals(key)) {
+ return LDValue.of(name);
+ }
+ if (parameters.getType() != LDValueType.OBJECT) {
+ return LDValue.ofNull();
+ }
+ return parameters.get(key);
+ }
+
+ /**
+ * Retrieves a custom value by key.
+ *
+ * @param key the custom data key
+ * @return the value, or {@link LDValue#ofNull()} if not present
+ */
+ public LDValue getCustom(String key) {
+ if (custom.getType() != LDValueType.OBJECT) {
+ return LDValue.ofNull();
+ }
+ return custom.get(key);
+ }
+
+ /**
+ * Returns the full set of model parameters.
+ *
+ * @return the parameters object, or {@link LDValue#ofNull()} if none were provided
+ */
+ public LDValue getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Returns the full set of custom data.
+ *
+ * @return the custom object, or {@link LDValue#ofNull()} if none was provided
+ */
+ public LDValue getCustom() {
+ return custom;
+ }
+
+ /**
+ * Renders this model config as an {@link LDValue} object.
+ *
+ * @return the JSON representation
+ */
+ public LDValue toLDValue() {
+ return LDValue.buildObject()
+ .put("name", name)
+ .put("parameters", parameters)
+ .put("custom", custom)
+ .build();
+ }
+
+ /**
+ * Creates a builder for a model configuration.
+ *
+ * @param name the name of the model
+ * @return a new builder
+ */
+ public static Builder builder(String name) {
+ return new Builder(name);
+ }
+
+ /**
+ * A builder for {@link ModelConfig} instances.
+ */
+ public static final class Builder {
+ private final String name;
+ private final ObjectBuilder parameters = LDValue.buildObject();
+ private final ObjectBuilder custom = LDValue.buildObject();
+ private boolean hasParameters = false;
+ private boolean hasCustom = false;
+
+ private Builder(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Adds a model parameter.
+ *
+ * @param key the parameter key
+ * @param value the parameter value
+ * @return this builder
+ */
+ public Builder parameter(String key, LDValue value) {
+ parameters.put(key, value);
+ hasParameters = true;
+ return this;
+ }
+
+ /**
+ * Adds a custom data entry.
+ *
+ * @param key the custom data key
+ * @param value the custom data value
+ * @return this builder
+ */
+ public Builder custom(String key, LDValue value) {
+ custom.put(key, value);
+ hasCustom = true;
+ return this;
+ }
+
+ /**
+ * Builds the model configuration.
+ *
+ * @return a new {@link ModelConfig}
+ */
+ public ModelConfig build() {
+ return new ModelConfig(
+ name,
+ hasParameters ? parameters.build() : LDValue.ofNull(),
+ hasCustom ? custom.build() : LDValue.ofNull());
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ModelConfig)) {
+ return false;
+ }
+ ModelConfig other = (ModelConfig) o;
+ return Objects.equals(name, other.name)
+ && Objects.equals(parameters, other.parameters)
+ && Objects.equals(custom, other.custom);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, parameters, custom);
+ }
+
+ @Override
+ public String toString() {
+ return "ModelConfig{name=" + name + ", parameters=" + parameters + ", custom=" + custom + "}";
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/ProviderConfig.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/ProviderConfig.java
new file mode 100644
index 00000000..d2827df1
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/ProviderConfig.java
@@ -0,0 +1,61 @@
+package com.launchdarkly.sdk.server.ai.datamodel;
+
+import com.launchdarkly.sdk.LDValue;
+
+import java.util.Objects;
+
+/**
+ * Configuration describing the AI provider associated with an AI Config (for example
+ * {@code "openai"}).
+ */
+public final class ProviderConfig {
+ private final String name;
+
+ /**
+ * Creates a provider configuration.
+ *
+ * @param name the name of the provider
+ */
+ public ProviderConfig(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of the provider.
+ *
+ * @return the provider name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Renders this provider config as an {@link LDValue} object.
+ *
+ * @return the JSON representation
+ */
+ public LDValue toLDValue() {
+ return LDValue.buildObject().put("name", name).build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ProviderConfig)) {
+ return false;
+ }
+ return Objects.equals(name, ((ProviderConfig) o).name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(name);
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderConfig{name=" + name + "}";
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/Evaluator.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/Evaluator.java
new file mode 100644
index 00000000..3fd8ee4e
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/Evaluator.java
@@ -0,0 +1,63 @@
+package com.launchdarkly.sdk.server.ai.evaluation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Coordinates multiple judge evaluations for a single AI Config invocation.
+ *
+ * Instances are created by the SDK and attached to {@code AICompletionConfig} and
+ * {@code AIAgentConfig} results, so callers can run all configured judges with a single call.
+ * Configurations without judges carry a {@link #noop() no-op} evaluator.
+ *
+ * The evaluator coordinates evaluations only; it does not perform any LaunchDarkly event tracking.
+ * Tracking of {@link JudgeResult} values is the responsibility of the caller (typically via
+ * {@code AIConfigTracker.trackJudgeResult}).
+ */
+public final class Evaluator {
+ private final List judges;
+
+ /**
+ * Creates an evaluator wrapping the given judges. Each judge applies its own sampling rate.
+ *
+ * @param judges the initialized judges
+ */
+ public Evaluator(List judges) {
+ this.judges = Collections.unmodifiableList(new ArrayList<>(judges));
+ }
+
+ /**
+ * Returns a no-op evaluator that resolves immediately to an empty result and invokes no judges.
+ *
+ * @return a no-op evaluator
+ */
+ public static Evaluator noop() {
+ return new Evaluator(Collections.emptyList());
+ }
+
+ /**
+ * Runs all configured judges against the input/output pair.
+ *
+ * Judges are evaluated in order; the returned future resolves once every judge has completed.
+ *
+ * @param input the input that was provided to the AI model
+ * @param output the AI-generated output to evaluate
+ * @return a future that resolves to one {@link JudgeResult} per configured judge, in order
+ */
+ public CompletableFuture> evaluate(String input, String output) {
+ if (judges.isEmpty()) {
+ return CompletableFuture.completedFuture(Collections.emptyList());
+ }
+ CompletableFuture> chain = CompletableFuture.completedFuture(new ArrayList<>());
+ for (Judge judge : judges) {
+ chain = chain.thenCompose(accumulated ->
+ judge.evaluate(input, output).thenApply(result -> {
+ accumulated.add(result);
+ return accumulated;
+ }));
+ }
+ return chain.thenApply(Collections::unmodifiableList);
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/Judge.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/Judge.java
new file mode 100644
index 00000000..3c0ec561
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/Judge.java
@@ -0,0 +1,33 @@
+package com.launchdarkly.sdk.server.ai.evaluation;
+
+import com.launchdarkly.sdk.server.ai.datamodel.AIJudgeConfig;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Evaluates AI outputs against a configured metric.
+ *
+ * A {@code Judge} pairs an {@link AIJudgeConfig} with a provider-specific model runner that performs
+ * the actual evaluation. Provider-backed implementations are supplied by AI provider integration
+ * packages; this interface is the seam through which they plug into the SDK's {@link Evaluator}.
+ */
+public interface Judge {
+ /**
+ * Evaluates the given input/output pair.
+ *
+ * The judge applies its own sampling rate: when an evaluation is skipped by sampling, the returned
+ * result has {@link JudgeResult#isSampled()} set to {@code false}.
+ *
+ * @param input the input that was provided to the AI model
+ * @param output the AI-generated output to evaluate
+ * @return a future that resolves to the evaluation result
+ */
+ CompletableFuture evaluate(String input, String output);
+
+ /**
+ * Returns the judge configuration retrieved during initialization.
+ *
+ * @return the judge configuration
+ */
+ AIJudgeConfig getConfig();
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/JudgeResult.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/JudgeResult.java
new file mode 100644
index 00000000..fe80e0c1
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/evaluation/JudgeResult.java
@@ -0,0 +1,175 @@
+package com.launchdarkly.sdk.server.ai.evaluation;
+
+/**
+ * The outcome of a single judge metric evaluation.
+ *
+ * When {@link #isSampled()} is {@code false}, the evaluation was bypassed by the judge's sampling
+ * rate and the remaining fields are at their default values and should not be treated as results.
+ */
+public final class JudgeResult {
+ private final String judgeConfigKey;
+ private final boolean success;
+ private final String errorMessage;
+ private final boolean sampled;
+ private final String metricKey;
+ private final Double score;
+ private final String reasoning;
+
+ private JudgeResult(Builder builder) {
+ this.judgeConfigKey = builder.judgeConfigKey;
+ this.success = builder.success;
+ this.errorMessage = builder.errorMessage;
+ this.sampled = builder.sampled;
+ this.metricKey = builder.metricKey;
+ this.score = builder.score;
+ this.reasoning = builder.reasoning;
+ }
+
+ /**
+ * Returns a result indicating the evaluation was skipped by the sampling rate.
+ *
+ * @return a not-sampled result
+ */
+ public static JudgeResult notSampled() {
+ return builder().sampled(false).success(false).build();
+ }
+
+ /**
+ * Returns the key of the judge configuration that produced this result.
+ *
+ * @return the judge config key, or {@code null} if not set
+ */
+ public String getJudgeConfigKey() {
+ return judgeConfigKey;
+ }
+
+ /**
+ * Returns whether the evaluation completed successfully.
+ *
+ * @return {@code true} if successful
+ */
+ public boolean isSuccess() {
+ return success;
+ }
+
+ /**
+ * Returns the error message if the evaluation failed.
+ *
+ * @return the error message, or {@code null}
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * Returns whether the evaluation was sampled and executed.
+ *
+ * @return {@code true} if the evaluation was performed
+ */
+ public boolean isSampled() {
+ return sampled;
+ }
+
+ /**
+ * Returns the metric key this result corresponds to.
+ *
+ * @return the metric key, or {@code null}
+ */
+ public String getMetricKey() {
+ return metricKey;
+ }
+
+ /**
+ * Returns the evaluation score, between {@code 0.0} and {@code 1.0}.
+ *
+ * @return the score, or {@code null} if not available
+ */
+ public Double getScore() {
+ return score;
+ }
+
+ /**
+ * Returns the reasoning behind the score.
+ *
+ * @return the reasoning, or {@code null}
+ */
+ public String getReasoning() {
+ return reasoning;
+ }
+
+ /**
+ * Creates a builder for a {@link JudgeResult}.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A builder for {@link JudgeResult} instances.
+ */
+ public static final class Builder {
+ private String judgeConfigKey;
+ private boolean success = false;
+ private String errorMessage;
+ private boolean sampled = false;
+ private String metricKey;
+ private Double score;
+ private String reasoning;
+
+ private Builder() {
+ }
+
+ /** @param judgeConfigKey the judge config key @return this builder */
+ public Builder judgeConfigKey(String judgeConfigKey) {
+ this.judgeConfigKey = judgeConfigKey;
+ return this;
+ }
+
+ /** @param success whether the evaluation succeeded @return this builder */
+ public Builder success(boolean success) {
+ this.success = success;
+ return this;
+ }
+
+ /** @param errorMessage the error message @return this builder */
+ public Builder errorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ return this;
+ }
+
+ /** @param sampled whether the evaluation was sampled and executed @return this builder */
+ public Builder sampled(boolean sampled) {
+ this.sampled = sampled;
+ return this;
+ }
+
+ /** @param metricKey the metric key @return this builder */
+ public Builder metricKey(String metricKey) {
+ this.metricKey = metricKey;
+ return this;
+ }
+
+ /** @param score the evaluation score @return this builder */
+ public Builder score(Double score) {
+ this.score = score;
+ return this;
+ }
+
+ /** @param reasoning the reasoning behind the score @return this builder */
+ public Builder reasoning(String reasoning) {
+ this.reasoning = reasoning;
+ return this;
+ }
+
+ /**
+ * Builds the result.
+ *
+ * @return a new {@link JudgeResult}
+ */
+ public JudgeResult build() {
+ return new JudgeResult(this);
+ }
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/AISdkInfo.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/AISdkInfo.java
new file mode 100644
index 00000000..4b665218
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/AISdkInfo.java
@@ -0,0 +1,24 @@
+package com.launchdarkly.sdk.server.ai.internal;
+
+/**
+ * Internal constants identifying this AI SDK package, reported once per {@code LDAIClient} via the
+ * {@code $ld:ai:sdk:info} event.
+ *
+ * This class is for internal use only and is not part of the supported public API.
+ */
+public final class AISdkInfo {
+ /** The published name of this AI SDK package. */
+ public static final String AI_SDK_NAME = "launchdarkly-java-server-sdk-ai";
+
+ /** The implementation language of this AI SDK. */
+ public static final String AI_SDK_LANGUAGE = "java";
+
+ /** The version of this AI SDK. */
+ // This constant is updated automatically by release-please when the project version changes.
+ // x-release-please-start-version
+ public static final String AI_SDK_VERSION = "0.1.0";
+ // x-release-please-end
+
+ private AISdkInfo() {
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/Interpolator.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/Interpolator.java
new file mode 100644
index 00000000..04811e4e
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/Interpolator.java
@@ -0,0 +1,55 @@
+package com.launchdarkly.sdk.server.ai.internal;
+
+import com.samskivert.mustache.Mustache;
+import com.samskivert.mustache.Mustache.Compiler;
+import com.samskivert.mustache.Mustache.Escaper;
+
+import java.util.Map;
+
+/**
+ * Internal helper that renders Mustache templates for AI Config message and instruction
+ * interpolation.
+ *
+ * The renderer is configured to match the behavior of the Python reference SDK (which uses the
+ * {@code chevron} library):
+ *
+ *
Missing or {@code null} variables render as the empty string rather than raising an error.
+ *
{@code {{ value }}} tags HTML-escape {@code &}, {@code <}, {@code >}, and {@code "} (and
+ * only those characters), while {@code {{{ value }}}} tags emit the raw value.
+ *
+ *
+ * This class is for internal use only and is not part of the supported public API.
+ */
+public final class Interpolator {
+ /**
+ * Escapes exactly the characters that the Python {@code chevron} library escapes, so that
+ * interpolated prompts are byte-for-byte compatible across SDKs.
+ */
+ private static final Escaper CHEVRON_ESCAPER = raw ->
+ raw.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\"", """);
+
+ // defaultValue("") makes both null-valued and entirely-missing variables render as the empty
+ // string (it sets nullValue="" and missingIsNull=true). A trailing nullValue("") must NOT be
+ // chained here, as that would reset missingIsNull and cause missing variables to throw.
+ private static final Compiler COMPILER = Mustache.compiler()
+ .escapeHTML(true)
+ .withEscaper(CHEVRON_ESCAPER)
+ .defaultValue("");
+
+ private Interpolator() {
+ }
+
+ /**
+ * Renders a Mustache template against the supplied variables.
+ *
+ * @param template the template string
+ * @param variables the variables available to the template
+ * @return the rendered string
+ */
+ public static String interpolate(String template, Map variables) {
+ return COMPILER.compile(template).execute(variables);
+ }
+}
diff --git a/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/LDValueConversions.java b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/LDValueConversions.java
new file mode 100644
index 00000000..e2b55485
--- /dev/null
+++ b/lib/java-server-sdk-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/LDValueConversions.java
@@ -0,0 +1,66 @@
+package com.launchdarkly.sdk.server.ai.internal;
+
+import com.launchdarkly.sdk.LDValue;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Internal helpers for converting {@link LDValue} instances to and from the plain Java objects
+ * (maps, lists, strings, numbers, booleans) understood by the Mustache renderer.
+ *
+ * This class is for internal use only and is not part of the supported public API.
+ */
+public final class LDValueConversions {
+ private LDValueConversions() {
+ }
+
+ /**
+ * Converts an {@link LDValue} into a plain Java object tree.
+ *
+ * @param value the value to convert (may be {@code null})
+ * @return {@code null}, a {@link Boolean}, a {@link Long} or {@link Double}, a {@link String},
+ * a {@link List}, or a {@link Map} depending on the value's JSON type
+ */
+ public static Object toPlainObject(LDValue value) {
+ if (value == null || value.isNull()) {
+ return null;
+ }
+ switch (value.getType()) {
+ case BOOLEAN:
+ return value.booleanValue();
+ case NUMBER:
+ return numberToPlainObject(value);
+ case STRING:
+ return value.stringValue();
+ case ARRAY: {
+ List