diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAgentAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAgentAttributesExtractor.java
new file mode 100644
index 000000000000..c4e14b528b48
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAgentAttributesExtractor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai;
+
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.AgentIncubatingAttributes.GEN_AI_AGENT_DESCRIPTION;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.AgentIncubatingAttributes.GEN_AI_AGENT_ID;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.AgentIncubatingAttributes.GEN_AI_AGENT_NAME;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.AgentIncubatingAttributes.GEN_AI_DATA_SOURCE_ID;
+import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet;
+
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
+import javax.annotation.Nullable;
+
+/**
+ * Extractor of GenAI Agent
+ * attributes.
+ *
+ *
This class delegates to a type-specific {@link GenAiAgentAttributesGetter} for individual
+ * attribute extraction from request/response objects.
+ */
+public final class GenAiAgentAttributesExtractor
+ implements AttributesExtractor {
+
+ /** Creates the GenAI Agent attributes extractor. */
+ public static AttributesExtractor create(
+ GenAiAgentAttributesGetter attributesGetter) {
+ return new GenAiAgentAttributesExtractor<>(attributesGetter);
+ }
+
+ private final GenAiAgentAttributesGetter getter;
+
+ private GenAiAgentAttributesExtractor(GenAiAgentAttributesGetter getter) {
+ this.getter = getter;
+ }
+
+ @Override
+ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
+ internalSet(attributes, GEN_AI_AGENT_ID, getter.getId(request));
+ internalSet(attributes, GEN_AI_AGENT_NAME, getter.getName(request));
+ internalSet(attributes, GEN_AI_AGENT_DESCRIPTION, getter.getDescription(request));
+ internalSet(attributes, GEN_AI_DATA_SOURCE_ID, getter.getDataSourceId(request));
+ }
+
+ @Override
+ public void onEnd(
+ AttributesBuilder attributes,
+ Context context,
+ REQUEST request,
+ @Nullable RESPONSE response,
+ @Nullable Throwable error) {
+ // do nothing
+ }
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAgentAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAgentAttributesGetter.java
new file mode 100644
index 000000000000..d7837cd57341
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAgentAttributesGetter.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai;
+
+public interface GenAiAgentAttributesGetter {
+
+ String getName(REQUEST request);
+
+ @Nullable
+ String getDescription(REQUEST request);
+
+ @Nullable
+ String getId(REQUEST request);
+
+ @Nullable
+ String getDataSourceId(REQUEST request);
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java
index ed2e48cd8024..d333f8c0eef5 100644
--- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java
@@ -15,8 +15,8 @@
* library/framework. It will be used by the {@link GenAiAttributesExtractor} to obtain the various
* GenAI attributes in a type-generic way.
*/
-public interface GenAiAttributesGetter {
- String getOperationName(REQUEST request);
+public interface GenAiAttributesGetter
+ extends GenAiOperationAttributesGetter {
String getSystem(REQUEST request);
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiMessagesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiMessagesExtractor.java
new file mode 100644
index 000000000000..a536880f8283
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiMessagesExtractor.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai;
+
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_INPUT_MESSAGES;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_OPERATION_NAME;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_OUTPUT_MESSAGES;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_OUTPUT_TYPE;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_PROVIDER_NAME;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_CHOICE_COUNT;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_MAX_TOKENS;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_MODEL;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_SEED;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_STOP_SEQUENCES;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_TEMPERATURE;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_TOP_K;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_TOP_P;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_RESPONSE_FINISH_REASONS;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_RESPONSE_ID;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_RESPONSE_MODEL;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_SYSTEM_INSTRUCTIONS;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_TOOL_DEFINITIONS;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_INPUT_TOKENS;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_OUTPUT_TOKENS;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GenAiEventName.GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS;
+import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EVENT_NAME;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.logs.LogRecordBuilder;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.aliyun.common.JsonMarshaler;
+import io.opentelemetry.instrumentation.api.aliyun.common.provider.GlobalInstanceHolder;
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions;
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions.CaptureMessageStrategy;
+import io.opentelemetry.instrumentation.api.genai.messages.InputMessages;
+import io.opentelemetry.instrumentation.api.genai.messages.OutputMessages;
+import io.opentelemetry.instrumentation.api.genai.messages.SystemInstructions;
+import io.opentelemetry.instrumentation.api.genai.messages.ToolDefinitions;
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
+import io.opentelemetry.instrumentation.api.log.genai.GenAiEventLoggerProvider;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+public class GenAiMessagesExtractor
+ implements AttributesExtractor {
+
+ private static final Logger LOGGER = Logger.getLogger(GenAiMessagesExtractor.class.getName());
+
+ /** Creates the GenAI attributes extractor. */
+ public static AttributesExtractor create(
+ GenAiAttributesGetter attributesGetter,
+ GenAiMessagesProvider messagesProvider,
+ MessageCaptureOptions messageCaptureOptions,
+ String instrumentationName) {
+ return new GenAiMessagesExtractor<>(
+ attributesGetter, messagesProvider, messageCaptureOptions, instrumentationName);
+ }
+
+ private final MessageCaptureOptions messageCaptureOptions;
+
+ private final GenAiAttributesGetter getter;
+
+ private final GenAiMessagesProvider messagesProvider;
+
+ private final String instrumentationName;
+
+ private final AtomicBoolean lazyInit = new AtomicBoolean(false);
+
+ private JsonMarshaler jsonMarshaler;
+
+ private io.opentelemetry.api.logs.Logger eventLogger;
+
+ private GenAiMessagesExtractor(
+ GenAiAttributesGetter getter,
+ GenAiMessagesProvider messagesProvider,
+ MessageCaptureOptions messageCaptureOptions,
+ String instrumentationName) {
+ this.getter = getter;
+ this.messagesProvider = messagesProvider;
+ this.messageCaptureOptions = messageCaptureOptions;
+ this.instrumentationName = instrumentationName;
+ }
+
+ @Override
+ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
+ tryInit();
+ if (CaptureMessageStrategy.SPAN_ATTRIBUTES.equals(
+ messageCaptureOptions.captureMessageStrategy())) {
+ SystemInstructions systemInstructions = messagesProvider.systemInstructions(request, null);
+ if (systemInstructions != null) {
+ internalSet(
+ attributes,
+ GEN_AI_SYSTEM_INSTRUCTIONS,
+ toJsonString(systemInstructions.getSerializableObject()));
+ }
+
+ InputMessages inputMessages = messagesProvider.inputMessages(request, null);
+ if (inputMessages != null) {
+ internalSet(
+ attributes, GEN_AI_INPUT_MESSAGES, toJsonString(inputMessages.getSerializableObject()));
+ }
+
+ ToolDefinitions toolDefinitions = messagesProvider.toolDefinitions(request, null);
+ if (toolDefinitions != null) {
+ internalSet(
+ attributes,
+ GEN_AI_TOOL_DEFINITIONS,
+ toJsonString(toolDefinitions.getSerializableObject()));
+ }
+ }
+ }
+
+ @Override
+ public void onEnd(
+ AttributesBuilder attributes,
+ Context context,
+ REQUEST request,
+ @Nullable RESPONSE response,
+ @Nullable Throwable error) {
+ if (CaptureMessageStrategy.SPAN_ATTRIBUTES.equals(
+ messageCaptureOptions.captureMessageStrategy())) {
+ OutputMessages outputMessages = messagesProvider.outputMessages(request, response);
+ if (outputMessages != null) {
+ internalSet(
+ attributes,
+ GEN_AI_OUTPUT_MESSAGES,
+ toJsonString(outputMessages.getSerializableObject()));
+ }
+ } else if (CaptureMessageStrategy.EVENT.equals(
+ messageCaptureOptions.captureMessageStrategy())) {
+ emitInferenceEvent(context, request, response);
+ }
+ }
+
+ private void emitInferenceEvent(Context context, REQUEST request, @Nullable RESPONSE response) {
+ if (eventLogger != null) {
+ LogRecordBuilder builder =
+ eventLogger
+ .logRecordBuilder()
+ .setAttribute(EVENT_NAME, GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS)
+ .setContext(context);
+
+ SystemInstructions systemInstructions =
+ messagesProvider.systemInstructions(request, response);
+ if (systemInstructions != null) {
+ internalSetLogAttribute(
+ builder,
+ GEN_AI_SYSTEM_INSTRUCTIONS,
+ toJsonString(systemInstructions.getSerializableObject()));
+ }
+ InputMessages inputMessages = messagesProvider.inputMessages(request, response);
+ if (inputMessages != null) {
+ internalSetLogAttribute(
+ builder, GEN_AI_INPUT_MESSAGES, toJsonString(inputMessages.getSerializableObject()));
+ }
+ ToolDefinitions toolDefinitions = messagesProvider.toolDefinitions(request, null);
+ if (toolDefinitions != null) {
+ internalSetLogAttribute(
+ builder,
+ GEN_AI_TOOL_DEFINITIONS,
+ toJsonString(toolDefinitions.getSerializableObject()));
+ }
+ OutputMessages outputMessages = messagesProvider.outputMessages(request, response);
+ if (outputMessages != null) {
+ internalSetLogAttribute(
+ builder, GEN_AI_OUTPUT_MESSAGES, toJsonString(outputMessages.getSerializableObject()));
+ }
+
+ internalSetLogAttribute(builder, GEN_AI_OPERATION_NAME, getter.getOperationName(request));
+ internalSetLogAttribute(builder, GEN_AI_OUTPUT_TYPE, getter.getOutputType(request));
+ internalSetLogAttribute(builder, GEN_AI_REQUEST_CHOICE_COUNT, getter.getChoiceCount(request));
+ internalSetLogAttribute(builder, GEN_AI_PROVIDER_NAME, getter.getSystem(request));
+ internalSetLogAttribute(builder, GEN_AI_REQUEST_MODEL, getter.getRequestModel(request));
+ internalSetLogAttribute(builder, GEN_AI_REQUEST_SEED, getter.getRequestSeed(request));
+ internalSetLogAttribute(
+ builder, GEN_AI_REQUEST_FREQUENCY_PENALTY, getter.getRequestFrequencyPenalty(request));
+ internalSetLogAttribute(
+ builder, GEN_AI_REQUEST_MAX_TOKENS, getter.getRequestMaxTokens(request));
+ internalSetLogAttribute(
+ builder, GEN_AI_REQUEST_PRESENCE_PENALTY, getter.getRequestPresencePenalty(request));
+ internalSetLogAttribute(
+ builder, GEN_AI_REQUEST_STOP_SEQUENCES, getter.getRequestStopSequences(request));
+ internalSetLogAttribute(
+ builder, GEN_AI_REQUEST_TEMPERATURE, getter.getRequestTemperature(request));
+ internalSetLogAttribute(builder, GEN_AI_REQUEST_TOP_K, getter.getRequestTopK(request));
+ internalSetLogAttribute(builder, GEN_AI_REQUEST_TOP_P, getter.getRequestTopP(request));
+
+ List finishReasons = getter.getResponseFinishReasons(request, response);
+ if (finishReasons != null && !finishReasons.isEmpty()) {
+ builder.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, finishReasons);
+ }
+ internalSetLogAttribute(builder, GEN_AI_RESPONSE_ID, getter.getResponseId(request, response));
+ internalSetLogAttribute(
+ builder, GEN_AI_RESPONSE_MODEL, getter.getResponseModel(request, response));
+ internalSetLogAttribute(
+ builder, GEN_AI_USAGE_INPUT_TOKENS, getter.getUsageInputTokens(request, response));
+ internalSetLogAttribute(
+ builder, GEN_AI_USAGE_OUTPUT_TOKENS, getter.getUsageOutputTokens(request, response));
+ builder.emit();
+ }
+ }
+
+ private void internalSetLogAttribute(
+ LogRecordBuilder logRecordBuilder, AttributeKey key, @Nullable T value) {
+ if (value == null) {
+ return;
+ }
+ logRecordBuilder.setAttribute(key, value);
+ }
+
+ private void tryInit() {
+ if (lazyInit.get()) {
+ return;
+ }
+
+ if (lazyInit.compareAndSet(false, true)) {
+ jsonMarshaler = GlobalInstanceHolder.getInstance(JsonMarshaler.class);
+ if (jsonMarshaler == null) {
+ LOGGER.log(Level.WARNING, "failed to init json marshaler, global instance is null");
+ }
+
+ GenAiEventLoggerProvider loggerProvider =
+ GlobalInstanceHolder.getInstance(GenAiEventLoggerProvider.class);
+
+ if (loggerProvider == null) {
+ LOGGER.log(Level.WARNING, "failed to init event logger, logger provider is null");
+ return;
+ }
+
+ eventLogger = loggerProvider.get(instrumentationName);
+ }
+ }
+
+ private String toJsonString(Object object) {
+ if (jsonMarshaler == null) {
+ LOGGER.log(Level.INFO, "failed to serialize object, json marshaler is null");
+ return null;
+ }
+ return jsonMarshaler.toJSONStringNonEmpty(object);
+ }
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiMessagesProvider.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiMessagesProvider.java
new file mode 100644
index 000000000000..5730640d46d3
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiMessagesProvider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai;
+
+import io.opentelemetry.instrumentation.api.genai.messages.InputMessages;
+import io.opentelemetry.instrumentation.api.genai.messages.OutputMessages;
+import io.opentelemetry.instrumentation.api.genai.messages.SystemInstructions;
+import io.opentelemetry.instrumentation.api.genai.messages.ToolDefinitions;
+import javax.annotation.Nullable;
+
+public interface GenAiMessagesProvider {
+
+ @Nullable
+ InputMessages inputMessages(REQUEST request, @Nullable RESPONSE response);
+
+ @Nullable
+ OutputMessages outputMessages(REQUEST request, @Nullable RESPONSE response);
+
+ @Nullable
+ SystemInstructions systemInstructions(REQUEST request, @Nullable RESPONSE response);
+
+ @Nullable
+ ToolDefinitions toolDefinitions(REQUEST request, @Nullable RESPONSE response);
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiOperationAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiOperationAttributesGetter.java
new file mode 100644
index 000000000000..7ac899401077
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiOperationAttributesGetter.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai;
+
+public interface GenAiOperationAttributesGetter {
+
+ String getOperationName(REQUEST request);
+
+ @Nullable
+ String getOperationTarget(REQUEST request);
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiSpanNameExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiSpanNameExtractor.java
index d8a7f517da3c..66ebb9949c6c 100644
--- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiSpanNameExtractor.java
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiSpanNameExtractor.java
@@ -19,19 +19,19 @@ public static SpanNameExtractor create(
return new GenAiSpanNameExtractor<>(attributesGetter);
}
- private final GenAiAttributesGetter getter;
+ private final GenAiOperationAttributesGetter getter;
- private GenAiSpanNameExtractor(GenAiAttributesGetter getter) {
+ private GenAiSpanNameExtractor(GenAiOperationAttributesGetter getter) {
this.getter = getter;
}
@Override
public String extract(REQUEST request) {
String operation = getter.getOperationName(request);
- String model = getter.getRequestModel(request);
- if (model == null) {
+ String operationTarget = getter.getOperationTarget(request);
+ if (operationTarget == null) {
return operation;
}
- return operation + ' ' + model;
+ return operation + ' ' + operationTarget;
}
}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/AgentIncubatingAttributes.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/AgentIncubatingAttributes.java
new file mode 100644
index 000000000000..3c40e1ead0f1
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/AgentIncubatingAttributes.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai.incubator;
+
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
+
+import io.opentelemetry.api.common.AttributeKey;
+
+public final class AgentIncubatingAttributes {
+
+ public static final AttributeKey GEN_AI_AGENT_DESCRIPTION =
+ stringKey("gen_ai.agent.description");
+ public static final AttributeKey GEN_AI_AGENT_ID = stringKey("gen_ai.agent.id");
+ public static final AttributeKey GEN_AI_AGENT_NAME = stringKey("gen_ai.agent.name");
+ public static final AttributeKey GEN_AI_DATA_SOURCE_ID =
+ stringKey("gen_ai.data_source.id");
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/GenAiIncubatingAttributes.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/GenAiIncubatingAttributes.java
new file mode 100644
index 000000000000..507315726c22
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/GenAiIncubatingAttributes.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai.incubator;
+
+import static io.opentelemetry.api.common.AttributeKey.doubleKey;
+import static io.opentelemetry.api.common.AttributeKey.longKey;
+import static io.opentelemetry.api.common.AttributeKey.stringArrayKey;
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
+
+import io.opentelemetry.api.common.AttributeKey;
+import java.util.List;
+
+public final class GenAiIncubatingAttributes {
+
+ public static final AttributeKey GEN_AI_OPERATION_NAME =
+ stringKey("gen_ai.operation.name");
+ public static final AttributeKey> GEN_AI_REQUEST_ENCODING_FORMATS =
+ stringArrayKey("gen_ai.request.encoding_formats");
+ public static final AttributeKey GEN_AI_REQUEST_FREQUENCY_PENALTY =
+ doubleKey("gen_ai.request.frequency_penalty");
+ public static final AttributeKey GEN_AI_REQUEST_MAX_TOKENS =
+ longKey("gen_ai.request.max_tokens");
+ public static final AttributeKey GEN_AI_REQUEST_MODEL = stringKey("gen_ai.request.model");
+ public static final AttributeKey GEN_AI_REQUEST_PRESENCE_PENALTY =
+ doubleKey("gen_ai.request.presence_penalty");
+ public static final AttributeKey GEN_AI_REQUEST_SEED = longKey("gen_ai.request.seed");
+ public static final AttributeKey> GEN_AI_REQUEST_STOP_SEQUENCES =
+ stringArrayKey("gen_ai.request.stop_sequences");
+ public static final AttributeKey GEN_AI_REQUEST_TEMPERATURE =
+ doubleKey("gen_ai.request.temperature");
+ public static final AttributeKey GEN_AI_REQUEST_TOP_K = doubleKey("gen_ai.request.top_k");
+ public static final AttributeKey GEN_AI_REQUEST_TOP_P = doubleKey("gen_ai.request.top_p");
+ public static final AttributeKey> GEN_AI_RESPONSE_FINISH_REASONS =
+ stringArrayKey("gen_ai.response.finish_reasons");
+ public static final AttributeKey GEN_AI_RESPONSE_ID = stringKey("gen_ai.response.id");
+ public static final AttributeKey GEN_AI_RESPONSE_MODEL =
+ stringKey("gen_ai.response.model");
+ public static final AttributeKey GEN_AI_PROVIDER_NAME = stringKey("gen_ai.provider.name");
+ public static final AttributeKey GEN_AI_CONVERSATION_ID =
+ stringKey("gen_ai.conversation.id");
+ public static final AttributeKey GEN_AI_USAGE_INPUT_TOKENS =
+ longKey("gen_ai.usage.input_tokens");
+ public static final AttributeKey GEN_AI_USAGE_OUTPUT_TOKENS =
+ longKey("gen_ai.usage.output_tokens");
+ public static final AttributeKey GEN_AI_REQUEST_CHOICE_COUNT =
+ longKey("gen_ai.request.choice.count");
+ public static final AttributeKey GEN_AI_OUTPUT_TYPE = stringKey("gen_ai.output.type");
+ public static final AttributeKey GEN_AI_SYSTEM_INSTRUCTIONS =
+ stringKey("gen_ai.system_instructions");
+ public static final AttributeKey GEN_AI_INPUT_MESSAGES =
+ stringKey("gen_ai.input.messages");
+ public static final AttributeKey GEN_AI_OUTPUT_MESSAGES =
+ stringKey("gen_ai.output.messages");
+ public static final AttributeKey GEN_AI_TOOL_DEFINITIONS =
+ stringKey("gen_ai.tool.definitions");
+
+ public static class GenAiOperationNameIncubatingValues {
+ public static final String CHAT = "chat";
+ public static final String CREATE_AGENT = "create_agent";
+ public static final String EMBEDDINGS = "embeddings";
+ public static final String EXECUTE_TOOL = "execute_tool";
+ public static final String GENERATE_CONTENT = "generate_content";
+ public static final String INVOKE_AGENT = "invoke_agent";
+ public static final String TEXT_COMPLETION = "text_completion";
+ }
+
+ public static class GenAiProviderNameIncubatingValues {
+ public static final String ANTHROPIC = "anthropic";
+ public static final String AWS_BEDROCK = "aws.bedrock";
+ public static final String AZURE_AI_INFERENCE = "azure.ai.inference";
+ public static final String AZURE_AI_OPENAI = "azure.ai.openai";
+ public static final String COHERE = "cohere";
+ public static final String DEEPSEEK = "deepseek";
+ public static final String GCP_GEMINI = "gcp.gemini";
+ public static final String GCP_GEN_AI = "gcp.gen_ai";
+ public static final String GCP_VERTEX_AI = "gcp.vertex_ai";
+ public static final String GROQ = "groq";
+ public static final String IBM_WATSONX_AI = "ibm.watsonx.ai";
+ public static final String MISTRAL_AI = "mistral_ai";
+ public static final String OPENAI = "openai";
+ public static final String PERPLEXITY = "perplexity";
+ public static final String X_AI = "x_ai";
+ public static final String DASHSCOPE = "dashscope";
+ }
+
+ public static class GenAiEventName {
+ public static final String GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS =
+ "gen_ai.client.inference.operation.details";
+ }
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/GenAiToolIncubatingAttributes.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/GenAiToolIncubatingAttributes.java
new file mode 100644
index 000000000000..42eab245b3b7
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/incubator/GenAiToolIncubatingAttributes.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai.incubator;
+
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
+
+import io.opentelemetry.api.common.AttributeKey;
+
+public class GenAiToolIncubatingAttributes {
+
+ public static final AttributeKey GEN_AI_TOOL_CALL_ID = stringKey("gen_ai.tool.call.id");
+ public static final AttributeKey GEN_AI_TOOL_DESCRIPTION =
+ stringKey("gen_ai.tool.description");
+ public static final AttributeKey GEN_AI_TOOL_NAME = stringKey("gen_ai.tool.name");
+ public static final AttributeKey GEN_AI_TOOL_TYPE = stringKey("gen_ai.tool.type");
+ public static final AttributeKey GEN_AI_TOOL_CALL_ARGUMENTS =
+ stringKey("gen_ai.tool.call.arguments");
+ public static final AttributeKey GEN_AI_TOOL_CALL_RESULT =
+ stringKey("gen_ai.tool.call.result");
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/tool/GenAiToolAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/tool/GenAiToolAttributesExtractor.java
new file mode 100644
index 000000000000..d06a4e4e9ead
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/tool/GenAiToolAttributesExtractor.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai.tool;
+
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiIncubatingAttributes.GEN_AI_OPERATION_NAME;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiToolIncubatingAttributes.GEN_AI_TOOL_CALL_ARGUMENTS;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiToolIncubatingAttributes.GEN_AI_TOOL_CALL_ID;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiToolIncubatingAttributes.GEN_AI_TOOL_CALL_RESULT;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiToolIncubatingAttributes.GEN_AI_TOOL_DESCRIPTION;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiToolIncubatingAttributes.GEN_AI_TOOL_NAME;
+import static io.opentelemetry.instrumentation.api.instrumenter.genai.incubating.GenAiToolIncubatingAttributes.GEN_AI_TOOL_TYPE;
+import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet;
+
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions;
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
+import javax.annotation.Nullable;
+
+public final class GenAiToolAttributesExtractor
+ implements AttributesExtractor {
+
+ /** Creates the GenAI attributes extractor. */
+ public static AttributesExtractor create(
+ GenAiToolAttributesGetter attributesGetter,
+ MessageCaptureOptions messageCaptureOptions) {
+ return new GenAiToolAttributesExtractor<>(attributesGetter, messageCaptureOptions);
+ }
+
+ private final GenAiToolAttributesGetter getter;
+
+ private final MessageCaptureOptions messageCaptureOptions;
+
+ private GenAiToolAttributesExtractor(
+ GenAiToolAttributesGetter getter,
+ MessageCaptureOptions messageCaptureOptions) {
+ this.getter = getter;
+ this.messageCaptureOptions = messageCaptureOptions;
+ }
+
+ @Override
+ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
+ internalSet(attributes, GEN_AI_OPERATION_NAME, getter.getOperationName(request));
+ internalSet(attributes, GEN_AI_TOOL_DESCRIPTION, getter.getToolDescription(request));
+ internalSet(attributes, GEN_AI_TOOL_NAME, getter.getToolName(request));
+ internalSet(attributes, GEN_AI_TOOL_TYPE, getter.getToolType(request));
+ if (messageCaptureOptions.captureMessageContent()) {
+ internalSet(attributes, GEN_AI_TOOL_CALL_ARGUMENTS, getter.getToolCallArguments(request));
+ }
+ }
+
+ @Override
+ public void onEnd(
+ AttributesBuilder attributes,
+ Context context,
+ REQUEST request,
+ @Nullable RESPONSE response,
+ @Nullable Throwable error) {
+ internalSet(attributes, GEN_AI_TOOL_CALL_ID, getter.getToolCallId(request, response));
+ if (messageCaptureOptions.captureMessageContent()) {
+ internalSet(attributes, GEN_AI_TOOL_CALL_RESULT, getter.getToolCallResult(request, response));
+ }
+ }
+}
diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/tool/GenAiToolAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/tool/GenAiToolAttributesGetter.java
new file mode 100644
index 000000000000..53b181e8d6e7
--- /dev/null
+++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/tool/GenAiToolAttributesGetter.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.incubator.semconv.genai.tool;
+
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiOperationAttributesGetter;
+import javax.annotation.Nullable;
+
+public interface GenAiToolAttributesGetter
+ extends GenAiOperationAttributesGetter {
+
+ String getToolDescription(REQUEST request);
+
+ String getToolName(REQUEST request);
+
+ String getToolType(REQUEST request);
+
+ @Nullable
+ String getToolCallArguments(REQUEST request);
+
+ @Nullable
+ String getToolCallId(REQUEST request, RESPONSE response);
+
+ @Nullable
+ String getToolCallResult(REQUEST request, RESPONSE response);
+}
diff --git a/instrumentation/reactor/reactor-3.1/bootstrap/build.gradle.kts b/instrumentation/reactor/reactor-3.1/bootstrap/build.gradle.kts
new file mode 100644
index 000000000000..072a96df450f
--- /dev/null
+++ b/instrumentation/reactor/reactor-3.1/bootstrap/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins {
+ id("otel.javaagent-bootstrap")
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/build.gradle.kts
new file mode 100644
index 000000000000..697cb13f2302
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/build.gradle.kts
@@ -0,0 +1,71 @@
+plugins {
+ id("otel.javaagent-instrumentation")
+}
+
+otelJava {
+ // Spring AI 3 requires java 17
+ minJavaVersionSupported.set(JavaVersion.VERSION_17)
+}
+
+muzzle {
+ pass {
+ group.set("org.springframework.ai")
+ module.set("spring-ai-client-chat")
+ versions.set("(,)")
+ }
+}
+
+repositories {
+ mavenLocal()
+ maven {
+ url = uri("https://repo.spring.io/milestone")
+ content {
+ includeGroup("org.springframework.ai")
+ includeGroup("org.springframework.boot")
+ includeGroup("org.springframework")
+ }
+ }
+ maven {
+ url = uri("https://repo.spring.io/snapshot")
+ content {
+ includeGroup("org.springframework.ai")
+ includeGroup("org.springframework.boot")
+ includeGroup("org.springframework")
+ }
+ mavenContent {
+ snapshotsOnly()
+ }
+ }
+ mavenCentral()
+}
+
+dependencies {
+ library("io.projectreactor:reactor-core:3.7.0")
+ library("org.springframework.ai:spring-ai-client-chat:1.0.0")
+ library("org.springframework.ai:spring-ai-model:1.0.0")
+
+ implementation(project(":instrumentation:reactor:reactor-3.1:library"))
+
+ bootstrap(project(":instrumentation:reactor:reactor-3.1:bootstrap"))
+
+ testInstrumentation(project(":instrumentation:spring:spring-ai:spring-ai-openai-1.0:javaagent"))
+ testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent"))
+ testImplementation(project(":instrumentation:spring:spring-ai:spring-ai-1.0:testing"))
+}
+
+tasks {
+ withType().configureEach {
+ val latestDepTest = findProperty("testLatestDeps") as Boolean
+ systemProperty("testLatestDeps", latestDepTest)
+ // spring ai requires java 17
+ if (latestDepTest) {
+ otelJava {
+ minJavaVersionSupported.set(JavaVersion.VERSION_17)
+ }
+ }
+
+ // TODO run tests both with and without genai message capture
+ systemProperty("otel.instrumentation.genai.capture-message-content", "true")
+ systemProperty("collectMetadata", findProperty("collectMetadata")?.toString() ?: "false")
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiInstrumentationModule.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiInstrumentationModule.java
new file mode 100644
index 000000000000..adfd63e00213
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiInstrumentationModule.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0;
+
+import static java.util.Arrays.asList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client.DefaultCallResponseSpecInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client.DefaultStreamResponseSpecInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool.DefaultToolCallingManagerInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool.ToolCallbackInstrumentation;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class SpringAiInstrumentationModule extends InstrumentationModule {
+
+ public SpringAiInstrumentationModule() {
+ super("spring-ai", "spring-ai-1.0");
+ }
+
+ @Override
+ public List typeInstrumentations() {
+ return asList(
+ new DefaultCallResponseSpecInstrumentation(),
+ new DefaultStreamResponseSpecInstrumentation(),
+ new ToolCallbackInstrumentation(),
+ new DefaultToolCallingManagerInstrumentation());
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiSingletons.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiSingletons.java
new file mode 100644
index 000000000000..ad016b51aec0
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiSingletons.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
+
+public final class SpringAiSingletons {
+ public static final SpringAiTelemetry TELEMETRY =
+ SpringAiTelemetry.builder(GlobalOpenTelemetry.get())
+ .setCaptureMessageContent(
+ InstrumentationConfig.get()
+ .getBoolean("otel.instrumentation.genai.capture-message-content", true))
+ .setContentMaxLength(
+ InstrumentationConfig.get()
+ .getInt("otel.instrumentation.genai.message-content.max-length", 8192))
+ .setCaptureMessageStrategy(
+ InstrumentationConfig.get()
+ .getString(
+ "otel.instrumentation.genai.message-content.capture-strategy",
+ "span-attributes"))
+ .build();
+
+ private SpringAiSingletons() {}
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiTelemetry.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiTelemetry.java
new file mode 100644
index 000000000000..83c254f69d9a
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiTelemetry.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool.ToolCallRequest;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+
+public final class SpringAiTelemetry {
+
+ public static SpringAiTelemetryBuilder builder(OpenTelemetry openTelemetry) {
+ return new SpringAiTelemetryBuilder(openTelemetry);
+ }
+
+ private final Instrumenter chatClientInstrumenter;
+ private final Instrumenter toolCallInstrumenter;
+ private final MessageCaptureOptions messageCaptureOptions;
+
+ SpringAiTelemetry(
+ Instrumenter chatClientInstrumenter,
+ Instrumenter toolCallInstrumenter,
+ MessageCaptureOptions messageCaptureOptions) {
+ this.chatClientInstrumenter = chatClientInstrumenter;
+ this.toolCallInstrumenter = toolCallInstrumenter;
+ this.messageCaptureOptions = messageCaptureOptions;
+ }
+
+ public Instrumenter chatClientInstrumenter() {
+ return chatClientInstrumenter;
+ }
+
+ public Instrumenter toolCallInstrumenter() {
+ return toolCallInstrumenter;
+ }
+
+ public MessageCaptureOptions messageCaptureOptions() {
+ return messageCaptureOptions;
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiTelemetryBuilder.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiTelemetryBuilder.java
new file mode 100644
index 000000000000..dbd2b146d9fc
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/SpringAiTelemetryBuilder.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiAgentAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiMessagesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiSpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.genai.tool.GenAiToolAttributesExtractor;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client.ChatClientAttributesGetter;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client.ChatClientMessagesProvider;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool.ToolCallAttributesGetter;
+import io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool.ToolCallRequest;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+
+public final class SpringAiTelemetryBuilder {
+
+ private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-ai-1.0";
+
+ private final OpenTelemetry openTelemetry;
+ private boolean captureMessageContent;
+
+ private int contentMaxLength;
+
+ private String captureMessageStrategy;
+
+ SpringAiTelemetryBuilder(OpenTelemetry openTelemetry) {
+ this.openTelemetry = openTelemetry;
+ }
+
+ /** Sets whether to capture message content in spans. Defaults to false. */
+ @CanIgnoreReturnValue
+ public SpringAiTelemetryBuilder setCaptureMessageContent(boolean captureMessageContent) {
+ this.captureMessageContent = captureMessageContent;
+ return this;
+ }
+
+ /** Sets the maximum length of message content to capture. Defaults to 8192. */
+ @CanIgnoreReturnValue
+ public SpringAiTelemetryBuilder setContentMaxLength(int contentMaxLength) {
+ this.contentMaxLength = contentMaxLength;
+ return this;
+ }
+
+ /** Sets the strategy to capture message content. Defaults to "span-attributes". */
+ @CanIgnoreReturnValue
+ public SpringAiTelemetryBuilder setCaptureMessageStrategy(String captureMessageStrategy) {
+ this.captureMessageStrategy = captureMessageStrategy;
+ return this;
+ }
+
+ public SpringAiTelemetry build() {
+ MessageCaptureOptions messageCaptureOptions =
+ MessageCaptureOptions.create(
+ captureMessageContent, contentMaxLength, captureMessageStrategy);
+
+ Instrumenter chatClientInstrumenter =
+ Instrumenter.builder(
+ openTelemetry,
+ INSTRUMENTATION_NAME,
+ GenAiSpanNameExtractor.create(ChatClientAttributesGetter.INSTANCE))
+ .addAttributesExtractor(
+ GenAiAttributesExtractor.create(ChatClientAttributesGetter.INSTANCE))
+ .addAttributesExtractor(
+ GenAiAgentAttributesExtractor.create(ChatClientAttributesGetter.INSTANCE))
+ .addAttributesExtractor(
+ GenAiMessagesExtractor.create(
+ ChatClientAttributesGetter.INSTANCE,
+ ChatClientMessagesProvider.create(messageCaptureOptions),
+ messageCaptureOptions,
+ INSTRUMENTATION_NAME))
+ .buildInstrumenter();
+
+ Instrumenter toolCallInstrumenter =
+ Instrumenter.builder(
+ openTelemetry,
+ INSTRUMENTATION_NAME,
+ GenAiSpanNameExtractor.create(ToolCallAttributesGetter.INSTANCE))
+ .addAttributesExtractor(
+ GenAiToolAttributesExtractor.create(
+ ToolCallAttributesGetter.INSTANCE, messageCaptureOptions))
+ .buildInstrumenter();
+
+ return new SpringAiTelemetry(
+ chatClientInstrumenter, toolCallInstrumenter, messageCaptureOptions);
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientAttributesGetter.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientAttributesGetter.java
new file mode 100644
index 000000000000..0f06be5ea993
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientAttributesGetter.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiAgentAttributesGetter;
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiAttributesGetter;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+
+public enum ChatClientAttributesGetter
+ implements
+ GenAiAttributesGetter,
+ GenAiAgentAttributesGetter {
+ INSTANCE;
+
+ @Override
+ public String getOperationName(ChatClientRequest request) {
+ return "invoke_agent";
+ }
+
+ @Override
+ public String getSystem(ChatClientRequest request) {
+ return "spring-ai";
+ }
+
+ @Nullable
+ @Override
+ public String getRequestModel(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getModel();
+ }
+
+ @Override
+ public String getOperationTarget(ChatClientRequest request) {
+ return getName(request);
+ }
+
+ @Nullable
+ @Override
+ public Long getRequestSeed(ChatClientRequest request) {
+ // Spring AI currently does not support seed parameter
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public List getRequestEncodingFormats(ChatClientRequest request) {
+ // Spring AI currently does not support encoding_formats parameter
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Double getRequestFrequencyPenalty(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getFrequencyPenalty();
+ }
+
+ @Nullable
+ @Override
+ public Long getRequestMaxTokens(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null
+ || request.prompt().getOptions().getMaxTokens() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getMaxTokens().longValue();
+ }
+
+ @Nullable
+ @Override
+ public Double getRequestPresencePenalty(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getPresencePenalty();
+ }
+
+ @Nullable
+ @Override
+ public List getRequestStopSequences(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getStopSequences();
+ }
+
+ @Nullable
+ @Override
+ public Double getRequestTemperature(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getTemperature();
+ }
+
+ @Nullable
+ @Override
+ public Double getRequestTopK(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null || request.prompt().getOptions().getTopK() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getTopK().doubleValue();
+ }
+
+ @Nullable
+ @Override
+ public Double getRequestTopP(ChatClientRequest request) {
+ if (request.prompt().getOptions() == null) {
+ return null;
+ }
+ return request.prompt().getOptions().getTopP();
+ }
+
+ @Override
+ public List getResponseFinishReasons(
+ ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (response == null
+ || response.chatResponse() == null
+ || response.chatResponse().getResult() == null
+ || response.chatResponse().getResult().getMetadata() == null
+ || response.chatResponse().getResult().getMetadata().getFinishReason() == null) {
+ return emptyList();
+ }
+
+ return singletonList(
+ response.chatResponse().getResult().getMetadata().getFinishReason().toLowerCase());
+ }
+
+ @Nullable
+ @Override
+ public String getResponseId(ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (response == null
+ || response.chatResponse() == null
+ || response.chatResponse().getMetadata() == null) {
+ return null;
+ }
+
+ return response.chatResponse().getMetadata().getId();
+ }
+
+ @Nullable
+ @Override
+ public String getResponseModel(ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (response == null
+ || response.chatResponse() == null
+ || response.chatResponse().getMetadata() == null
+ || response.chatResponse().getMetadata().getModel() == null
+ || response.chatResponse().getMetadata().getModel().isEmpty()) {
+ return null;
+ }
+
+ return response.chatResponse().getMetadata().getModel();
+ }
+
+ @Nullable
+ @Override
+ public Long getUsageInputTokens(
+ ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (response == null
+ || response.chatResponse() == null
+ || response.chatResponse().getMetadata() == null
+ || response.chatResponse().getMetadata().getUsage() == null
+ || response.chatResponse().getMetadata().getUsage().getPromptTokens() == null
+ || response.chatResponse().getMetadata().getUsage().getPromptTokens() == 0) {
+ return null;
+ }
+
+ return response.chatResponse().getMetadata().getUsage().getPromptTokens().longValue();
+ }
+
+ @Nullable
+ @Override
+ public Long getUsageOutputTokens(
+ ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (response == null
+ || response.chatResponse() == null
+ || response.chatResponse().getMetadata() == null
+ || response.chatResponse().getMetadata().getUsage() == null
+ || response.chatResponse().getMetadata().getUsage().getCompletionTokens() == null
+ || response.chatResponse().getMetadata().getUsage().getCompletionTokens() == 0) {
+ return null;
+ }
+
+ return response.chatResponse().getMetadata().getUsage().getCompletionTokens().longValue();
+ }
+
+ @Override
+ public String getName(ChatClientRequest request) {
+ return "spring_ai chat_client";
+ }
+
+ @Nullable
+ @Override
+ public String getDescription(ChatClientRequest request) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getId(ChatClientRequest request) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getDataSourceId(ChatClientRequest request) {
+ return null;
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientMessageBuffer.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientMessageBuffer.java
new file mode 100644
index 000000000000..c1887033b0fe
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientMessageBuffer.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client;
+
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.AssistantMessage.ToolCall;
+import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
+import org.springframework.ai.chat.model.Generation;
+
+final class ChatClientMessageBuffer {
+ private static final String TRUNCATE_FLAG = "...[truncated]";
+ private final int index;
+ private final MessageCaptureOptions messageCaptureOptions;
+
+ @Nullable private String finishReason;
+
+ @Nullable private StringBuilder rawContentBuffer;
+
+ @Nullable private Map toolCalls;
+
+ ChatClientMessageBuffer(int index, MessageCaptureOptions messageCaptureOptions) {
+ this.index = index;
+ this.messageCaptureOptions = messageCaptureOptions;
+ }
+
+ Generation toGeneration() {
+ List toolCalls;
+ if (this.toolCalls != null) {
+ toolCalls = new ArrayList<>(this.toolCalls.size());
+ for (Map.Entry entry : this.toolCalls.entrySet()) {
+ if (entry.getValue() != null) {
+ String arguments;
+ if (entry.getValue().function.arguments != null) {
+ arguments = entry.getValue().function.arguments.toString();
+ } else {
+ arguments = "";
+ }
+ if (entry.getValue().type == null) {
+ entry.getValue().type = "function";
+ }
+ if (entry.getValue().function.name == null) {
+ entry.getValue().function.name = "";
+ }
+ toolCalls.add(
+ new ToolCall(
+ entry.getValue().id,
+ entry.getValue().type,
+ entry.getValue().function.name,
+ arguments));
+ }
+ }
+ } else {
+ toolCalls = Collections.emptyList();
+ }
+
+ String content = "";
+
+ if (this.rawContentBuffer != null) {
+ content = this.rawContentBuffer.toString();
+ }
+
+ return new Generation(
+ new AssistantMessage(content, Collections.emptyMap(), toolCalls),
+ ChatGenerationMetadata.builder().finishReason(this.finishReason).build());
+ }
+
+ void append(Generation generation) {
+ AssistantMessage message = generation.getOutput();
+ if (message != null) {
+ if (this.messageCaptureOptions.captureMessageContent()) {
+ if (message.getText() != null) {
+ if (this.rawContentBuffer == null) {
+ this.rawContentBuffer = new StringBuilder();
+ }
+
+ String deltaContent = message.getText();
+ if (this.rawContentBuffer.length()
+ < this.messageCaptureOptions.maxMessageContentLength()) {
+ if (this.rawContentBuffer.length() + deltaContent.length()
+ >= this.messageCaptureOptions.maxMessageContentLength()) {
+ deltaContent =
+ deltaContent.substring(
+ 0,
+ this.messageCaptureOptions.maxMessageContentLength()
+ - this.rawContentBuffer.length());
+ this.rawContentBuffer.append(deltaContent).append(TRUNCATE_FLAG);
+ } else {
+ this.rawContentBuffer.append(deltaContent);
+ }
+ }
+ }
+ }
+
+ if (message.hasToolCalls()) {
+ if (this.toolCalls == null) {
+ this.toolCalls = new HashMap<>();
+ }
+
+ for (int i = 0; i < message.getToolCalls().size(); i++) {
+ ToolCall toolCall = message.getToolCalls().get(i);
+ ToolCallBuffer buffer =
+ this.toolCalls.computeIfAbsent(i, unused -> new ToolCallBuffer(toolCall.id()));
+
+ buffer.type = toolCall.type();
+ buffer.function.name = toolCall.name();
+ if (this.messageCaptureOptions.captureMessageContent()) {
+ if (buffer.function.arguments == null) {
+ buffer.function.arguments = new StringBuilder();
+ }
+ buffer.function.arguments.append(toolCall.arguments());
+ }
+ }
+ }
+ }
+
+ ChatGenerationMetadata metadata = generation.getMetadata();
+ if (metadata != null
+ && metadata.getFinishReason() != null
+ && !metadata.getFinishReason().isEmpty()) {
+ this.finishReason = metadata.getFinishReason();
+ }
+ }
+
+ private static class FunctionBuffer {
+ @Nullable String name;
+ @Nullable StringBuilder arguments;
+ }
+
+ private static class ToolCallBuffer {
+ final String id;
+ final FunctionBuffer function = new FunctionBuffer();
+ @Nullable String type;
+
+ ToolCallBuffer(String id) {
+ this.id = id;
+ }
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientMessagesProvider.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientMessagesProvider.java
new file mode 100644
index 000000000000..df203a5dd054
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientMessagesProvider.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client;
+
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions;
+import io.opentelemetry.instrumentation.api.genai.messages.InputMessage;
+import io.opentelemetry.instrumentation.api.genai.messages.InputMessages;
+import io.opentelemetry.instrumentation.api.genai.messages.MessagePart;
+import io.opentelemetry.instrumentation.api.genai.messages.OutputMessage;
+import io.opentelemetry.instrumentation.api.genai.messages.OutputMessages;
+import io.opentelemetry.instrumentation.api.genai.messages.Role;
+import io.opentelemetry.instrumentation.api.genai.messages.SystemInstructions;
+import io.opentelemetry.instrumentation.api.genai.messages.TextPart;
+import io.opentelemetry.instrumentation.api.genai.messages.ToolCallRequestPart;
+import io.opentelemetry.instrumentation.api.genai.messages.ToolCallResponsePart;
+import io.opentelemetry.instrumentation.api.genai.messages.ToolDefinition;
+import io.opentelemetry.instrumentation.api.genai.messages.ToolDefinitions;
+import io.opentelemetry.instrumentation.api.instrumenter.genai.GenAiMessagesProvider;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.AssistantMessage.ToolCall;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.MessageType;
+import org.springframework.ai.chat.messages.ToolResponseMessage;
+import org.springframework.ai.chat.messages.ToolResponseMessage.ToolResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+
+public class ChatClientMessagesProvider
+ implements GenAiMessagesProvider {
+
+ private static final String TRUNCATE_FLAG = "...[truncated]";
+
+ private final MessageCaptureOptions messageCaptureOptions;
+
+ ChatClientMessagesProvider(MessageCaptureOptions messageCaptureOptions) {
+ this.messageCaptureOptions = messageCaptureOptions;
+ }
+
+ public static ChatClientMessagesProvider create(MessageCaptureOptions messageCaptureOptions) {
+ return new ChatClientMessagesProvider(messageCaptureOptions);
+ }
+
+ @Nullable
+ @Override
+ public InputMessages inputMessages(
+ ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (!messageCaptureOptions.captureMessageContent()
+ || request.prompt().getInstructions() == null) {
+ return null;
+ }
+
+ InputMessages inputMessages = InputMessages.create();
+ for (Message msg : request.prompt().getInstructions()) {
+ if (msg.getMessageType() == MessageType.SYSTEM) {
+ inputMessages.append(
+ InputMessage.create(Role.SYSTEM, contentToMessageParts(msg.getText())));
+ } else if (msg.getMessageType() == MessageType.USER) {
+ inputMessages.append(InputMessage.create(Role.USER, contentToMessageParts(msg.getText())));
+ } else if (msg.getMessageType() == MessageType.ASSISTANT) {
+ AssistantMessage assistantMessage = (AssistantMessage) msg;
+ List messageParts = new ArrayList<>();
+
+ if (assistantMessage.getText() != null && !assistantMessage.getText().isEmpty()) {
+ messageParts.addAll(contentToMessageParts(assistantMessage.getText()));
+ }
+
+ if (assistantMessage.hasToolCalls()) {
+ messageParts.addAll(
+ assistantMessage.getToolCalls().stream()
+ .map(this::toolCallToMessagePart)
+ .collect(Collectors.toList()));
+ }
+ inputMessages.append(InputMessage.create(Role.ASSISTANT, messageParts));
+ } else if (msg.getMessageType() == MessageType.TOOL) {
+ ToolResponseMessage toolResponseMessage = (ToolResponseMessage) msg;
+ inputMessages.append(
+ InputMessage.create(
+ Role.TOOL, contentToMessageParts(toolResponseMessage.getResponses())));
+ }
+ }
+ return inputMessages;
+ }
+
+ @Nullable
+ @Override
+ public OutputMessages outputMessages(
+ ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (!messageCaptureOptions.captureMessageContent()
+ || response == null
+ || response.chatResponse() == null
+ || response.chatResponse().getResults() == null) {
+ return null;
+ }
+
+ OutputMessages outputMessages = OutputMessages.create();
+ for (Generation generation : response.chatResponse().getResults()) {
+ AssistantMessage message = generation.getOutput();
+ List messageParts = new ArrayList<>();
+ if (message != null) {
+ if (message.getText() != null && !message.getText().isEmpty()) {
+ messageParts.addAll(contentToMessageParts(message.getText()));
+ }
+
+ if (message.hasToolCalls()) {
+ messageParts.addAll(
+ message.getToolCalls().stream()
+ .map(this::toolCallToMessagePart)
+ .collect(Collectors.toList()));
+ }
+ }
+
+ outputMessages.append(
+ OutputMessage.create(
+ Role.ASSISTANT,
+ messageParts,
+ generation.getMetadata().getFinishReason().toLowerCase()));
+ }
+ return outputMessages;
+ }
+
+ @Nullable
+ @Override
+ public SystemInstructions systemInstructions(
+ ChatClientRequest request, @Nullable ChatClientResponse response) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public ToolDefinitions toolDefinitions(
+ ChatClientRequest request, @Nullable ChatClientResponse response) {
+ if (request.prompt().getOptions() == null
+ || !(request.prompt().getOptions() instanceof ToolCallingChatOptions options)) {
+ return null;
+ }
+
+ ToolDefinitions toolDefinitions = ToolDefinitions.create();
+
+ // See: org.springframework.ai.model.tool.DefaultToolCallingManager.resolveToolDefinitions
+ options.getToolCallbacks().stream()
+ .map(
+ toolCallback -> {
+ String name = toolCallback.getToolDefinition().name();
+ String type = "function";
+ if (messageCaptureOptions.captureMessageContent()) {
+ return ToolDefinition.create(
+ type, name, toolCallback.getToolDefinition().description(), null);
+ } else {
+ return ToolDefinition.create(type, name, null, null);
+ }
+ })
+ .filter(Objects::nonNull)
+ .forEach(toolDefinitions::append);
+
+ for (String toolName : options.getToolNames()) {
+ // Skip the tool if it is already present in the request toolCallbacks.
+ // That might happen if a tool is defined in the options
+ // both as a ToolCallback and as a tool name.
+ if (options.getToolCallbacks().stream()
+ .anyMatch(tool -> tool.getToolDefinition().name().equals(toolName))) {
+ continue;
+ }
+ toolDefinitions.append(ToolDefinition.create("function", toolName, null, null));
+ }
+
+ return toolDefinitions;
+ }
+
+ private List contentToMessageParts(String content) {
+ return Collections.singletonList(TextPart.create(truncateTextContent(content)));
+ }
+
+ private MessagePart toolCallToMessagePart(ToolCall call) {
+ if (call != null) {
+ return ToolCallRequestPart.create(call.id(), call.name(), call.arguments());
+ }
+ return ToolCallRequestPart.create("unknown_function");
+ }
+
+ private List contentToMessageParts(List toolResponses) {
+ if (toolResponses == null) {
+ return Collections.singletonList(ToolCallResponsePart.create(""));
+ }
+
+ return toolResponses.stream()
+ .map(
+ response ->
+ ToolCallResponsePart.create(
+ response.id(), truncateTextContent(response.responseData())))
+ .collect(Collectors.toList());
+ }
+
+ private String truncateTextContent(String content) {
+ if (!content.endsWith(TRUNCATE_FLAG)
+ && content.length() > messageCaptureOptions.maxMessageContentLength()) {
+ content =
+ content.substring(0, messageCaptureOptions.maxMessageContentLength()) + TRUNCATE_FLAG;
+ }
+ return content;
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientStreamListener.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientStreamListener.java
new file mode 100644
index 000000000000..10e81fab9011
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientStreamListener.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.genai.MessageCaptureOptions;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+import org.springframework.ai.chat.metadata.ChatResponseMetadata;
+import org.springframework.ai.chat.metadata.DefaultUsage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+
+public final class ChatClientStreamListener {
+
+ private final Context context;
+ private final ChatClientRequest request;
+ private final Instrumenter instrumenter;
+ private final MessageCaptureOptions messageCaptureOptions;
+ private final boolean newSpan;
+ private final AtomicBoolean hasEnded;
+ private final List chatClientMessageBuffers;
+
+ // Aggregated metadata
+ private final AtomicLong inputTokens = new AtomicLong(0);
+ private final AtomicLong outputTokens = new AtomicLong(0);
+ private final AtomicReference requestId = new AtomicReference<>();
+ private final AtomicReference model = new AtomicReference<>();
+
+ public ChatClientStreamListener(
+ Context context,
+ ChatClientRequest request,
+ Instrumenter instrumenter,
+ MessageCaptureOptions messageCaptureOptions,
+ boolean newSpan) {
+ this.context = context;
+ this.request = request;
+ this.instrumenter = instrumenter;
+ this.messageCaptureOptions = messageCaptureOptions;
+ this.newSpan = newSpan;
+ this.hasEnded = new AtomicBoolean();
+ this.chatClientMessageBuffers = new ArrayList<>();
+ }
+
+ public void onChunk(ChatClientResponse chatClientChunk) {
+ if (chatClientChunk == null || chatClientChunk.chatResponse() == null) {
+ return;
+ }
+
+ ChatResponse chunk = chatClientChunk.chatResponse();
+ if (chunk.getMetadata() != null) {
+ if (chunk.getMetadata().getId() != null) {
+ requestId.set(chunk.getMetadata().getId());
+ }
+ if (chunk.getMetadata().getUsage() != null) {
+ if (chunk.getMetadata().getUsage().getPromptTokens() != null) {
+ inputTokens.set(chunk.getMetadata().getUsage().getPromptTokens().longValue());
+ }
+ if (chunk.getMetadata().getUsage().getCompletionTokens() != null) {
+ outputTokens.set(chunk.getMetadata().getUsage().getCompletionTokens().longValue());
+ }
+ }
+ }
+
+ if (chunk.getResults() != null) {
+ List generations = chunk.getResults();
+ for (int i = 0; i < generations.size(); i++) {
+ while (chatClientMessageBuffers.size() <= i) {
+ chatClientMessageBuffers.add(null);
+ }
+ ChatClientMessageBuffer buffer = chatClientMessageBuffers.get(i);
+ if (buffer == null) {
+ buffer = new ChatClientMessageBuffer(i, messageCaptureOptions);
+ chatClientMessageBuffers.set(i, buffer);
+ }
+
+ buffer.append(generations.get(i));
+ }
+ }
+ }
+
+ public void endSpan(@Nullable Throwable error) {
+ // Use an atomic operation since close() type of methods are exposed to the user
+ // and can come from any thread.
+ if (!this.hasEnded.compareAndSet(false, true)) {
+ return;
+ }
+
+ if (this.chatClientMessageBuffers.isEmpty()) {
+ // Only happens if we got no chunks, so we have no response.
+ if (this.newSpan) {
+ this.instrumenter.end(this.context, this.request, null, error);
+ }
+ return;
+ }
+
+ Integer inputTokens = null;
+ if (this.inputTokens.get() > 0) {
+ inputTokens = (int) this.inputTokens.get();
+ }
+
+ Integer outputTokens = null;
+ if (this.outputTokens.get() > 0) {
+ outputTokens = (int) this.outputTokens.get();
+ }
+
+ List generations =
+ this.chatClientMessageBuffers.stream()
+ .map(ChatClientMessageBuffer::toGeneration)
+ .collect(Collectors.toList());
+
+ ChatClientResponse response =
+ ChatClientResponse.builder()
+ .chatResponse(
+ ChatResponse.builder()
+ .generations(generations)
+ .metadata(
+ ChatResponseMetadata.builder()
+ .usage(new DefaultUsage(inputTokens, outputTokens))
+ .id(requestId.get())
+ .model(model.get())
+ .build())
+ .build())
+ .build();
+
+ if (this.newSpan) {
+ this.instrumenter.end(this.context, this.request, response, error);
+ }
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientStreamWrapper.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientStreamWrapper.java
new file mode 100644
index 000000000000..8aff1300f28f
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/ChatClientStreamWrapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator;
+import org.springframework.ai.chat.client.ChatClientResponse;
+import reactor.core.publisher.Flux;
+
+public final class ChatClientStreamWrapper {
+
+ public static Flux wrap(
+ Flux originFlux,
+ ChatClientStreamListener streamListener,
+ Context context) {
+
+ Flux chatClientResponseFlux =
+ originFlux
+ .doOnNext(chunk -> streamListener.onChunk(chunk))
+ .doOnComplete(() -> streamListener.endSpan(null))
+ .doOnError(streamListener::endSpan);
+ return ContextPropagationOperator.runWithContext(chatClientResponseFlux, context);
+ }
+
+ private ChatClientStreamWrapper() {}
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/DefaultCallResponseSpecInstrumentation.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/DefaultCallResponseSpecInstrumentation.java
new file mode 100644
index 000000000000..02a2c8db6796
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/DefaultCallResponseSpecInstrumentation.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.SpringAiSingletons.TELEMETRY;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isPrivate;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+
+@AutoService(TypeInstrumentation.class)
+public class DefaultCallResponseSpecInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher classLoaderOptimization() {
+ return hasClassesNamed(
+ "org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec");
+ }
+
+ @Override
+ public ElementMatcher typeMatcher() {
+ return named("org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec");
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ isMethod()
+ .and(named("doGetObservableChatClientResponse"))
+ .and(takesArguments(2))
+ .and(isPrivate())
+ .and(takesArgument(0, named("org.springframework.ai.chat.client.ChatClientRequest"))),
+ this.getClass().getName() + "$DoGetObservableChatClientResponseAdvice");
+ }
+
+ @SuppressWarnings("unused")
+ public static class DoGetObservableChatClientResponseAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void doGetObservableChatClientResponseEnter(
+ @Advice.Argument(0) ChatClientRequest request,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ context = Context.current();
+
+ if (TELEMETRY.chatClientInstrumenter().shouldStart(context, request)) {
+ context = TELEMETRY.chatClientInstrumenter().start(context, request);
+ }
+ scope = context.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void doGetObservableChatClientResponseExit(
+ @Advice.Argument(0) ChatClientRequest request,
+ @Advice.Return ChatClientResponse response,
+ @Advice.Thrown Throwable throwable,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope == null) {
+ return;
+ }
+ scope.close();
+
+ TELEMETRY.chatClientInstrumenter().end(context, request, response, throwable);
+ }
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/DefaultStreamResponseSpecInstrumentation.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/DefaultStreamResponseSpecInstrumentation.java
new file mode 100644
index 000000000000..2624b2c71bca
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/chat/client/DefaultStreamResponseSpecInstrumentation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.chat.client;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.SpringAiSingletons.TELEMETRY;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isPrivate;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+import reactor.core.publisher.Flux;
+
+@AutoService(TypeInstrumentation.class)
+public class DefaultStreamResponseSpecInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher classLoaderOptimization() {
+ return hasClassesNamed(
+ "org.springframework.ai.chat.client.DefaultChatClient$DefaultStreamResponseSpec");
+ }
+
+ @Override
+ public ElementMatcher typeMatcher() {
+ return named("org.springframework.ai.chat.client.DefaultChatClient$DefaultStreamResponseSpec");
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ isMethod()
+ .and(named("doGetObservableFluxChatResponse"))
+ .and(takesArguments(1))
+ .and(isPrivate())
+ .and(takesArgument(0, named("org.springframework.ai.chat.client.ChatClientRequest"))),
+ this.getClass().getName() + "$DoGetObservableFluxChatResponseAdvice");
+ }
+
+ @SuppressWarnings("unused")
+ public static class DoGetObservableFluxChatResponseAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void doGetObservableFluxChatResponseEnter(
+ @Advice.Argument(0) ChatClientRequest request,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelStreamListener") ChatClientStreamListener streamListener) {
+ context = Context.current();
+
+ if (TELEMETRY.chatClientInstrumenter().shouldStart(context, request)) {
+ context = TELEMETRY.chatClientInstrumenter().start(context, request);
+ streamListener =
+ new ChatClientStreamListener(
+ context,
+ request,
+ TELEMETRY.chatClientInstrumenter(),
+ TELEMETRY.messageCaptureOptions(),
+ true);
+ }
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void doGetObservableFluxChatResponseExit(
+ @Advice.Argument(0) ChatClientRequest request,
+ @Advice.Return(readOnly = false) Flux response,
+ @Advice.Thrown Throwable throwable,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelStreamListener") ChatClientStreamListener streamListener) {
+
+ if (throwable != null) {
+ // In case of exception, directly call end
+ TELEMETRY.chatClientInstrumenter().end(context, request, null, throwable);
+ return;
+ }
+
+ if (streamListener != null) {
+ // Wrap the response to integrate the stream listener
+ response = ChatClientStreamWrapper.wrap(response, streamListener, context);
+ }
+ }
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/DefaultToolCallingManagerInstrumentation.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/DefaultToolCallingManagerInstrumentation.java
new file mode 100644
index 000000000000..df6a310a6328
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/DefaultToolCallingManagerInstrumentation.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import java.util.HashMap;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.springframework.ai.chat.messages.AssistantMessage;
+
+@AutoService(TypeInstrumentation.class)
+public class DefaultToolCallingManagerInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher classLoaderOptimization() {
+ return hasClassesNamed("org.springframework.ai.model.tool.DefaultToolCallingManager");
+ }
+
+ @Override
+ public ElementMatcher typeMatcher() {
+ return named("org.springframework.ai.model.tool.DefaultToolCallingManager");
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ isMethod()
+ .and(named("executeToolCall"))
+ .and(takesArguments(3))
+ .and(takesArgument(1, named("org.springframework.ai.chat.messages.AssistantMessage"))),
+ this.getClass().getName() + "$ExecuteToolCallAdvice");
+ }
+
+ @SuppressWarnings("unused")
+ public static class ExecuteToolCallAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void executeToolCallEnter(
+ @Advice.Argument(1) AssistantMessage assistantMessage,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+
+ context = Context.current();
+
+ if (assistantMessage != null && assistantMessage.getToolCalls() != null) {
+ Map toolNameToIdMap = new HashMap<>();
+
+ for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
+ if (toolCall.id() != null && toolCall.name() != null) {
+ toolNameToIdMap.put(toolCall.name(), toolCall.id());
+ }
+ }
+
+ // store tool call ids map to context
+ if (!toolNameToIdMap.isEmpty()) {
+ context = ToolCallContext.storeToolCalls(context, toolNameToIdMap);
+ }
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void executeToolCallExit(@Advice.Local("otelScope") Scope scope) {
+ if (scope == null) {
+ return;
+ }
+ scope.close();
+ }
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/ToolCallAttributesGetter.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/ToolCallAttributesGetter.java
new file mode 100644
index 000000000000..b9f0d2e49dac
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/ToolCallAttributesGetter.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool;
+
+import io.opentelemetry.instrumentation.api.instrumenter.genai.tool.GenAiToolAttributesGetter;
+import javax.annotation.Nullable;
+
+public enum ToolCallAttributesGetter implements GenAiToolAttributesGetter {
+ INSTANCE;
+
+ @Override
+ public String getOperationName(ToolCallRequest request) {
+ return request.getOperationName();
+ }
+
+ @Override
+ public String getOperationTarget(ToolCallRequest request) {
+ return getToolName(request);
+ }
+
+ @Override
+ public String getToolDescription(ToolCallRequest request) {
+ return request.getDescription();
+ }
+
+ @Override
+ public String getToolName(ToolCallRequest request) {
+ return request.getName();
+ }
+
+ @Override
+ public String getToolType(ToolCallRequest request) {
+ return "function";
+ }
+
+ @Nullable
+ @Override
+ public String getToolCallArguments(ToolCallRequest request) {
+ return request.getToolInput();
+ }
+
+ @Nullable
+ @Override
+ public String getToolCallId(ToolCallRequest request, String response) {
+ return request.getToolCallId();
+ }
+
+ @Nullable
+ @Override
+ public String getToolCallResult(ToolCallRequest request, String response) {
+ return response;
+ }
+}
diff --git a/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/ToolCallContext.java b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/ToolCallContext.java
new file mode 100644
index 000000000000..f9e04c52a316
--- /dev/null
+++ b/instrumentation/spring/spring-ai/spring-ai-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ai/v1_0/tool/ToolCallContext.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.ai.v1_0.tool;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+import java.util.Map;
+
+/** Tool call context to store tool call ids map */
+public final class ToolCallContext {
+
+ private static final ContextKey