Skip to content

Instrument embeddings in openai client #14353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public interface GenAiAttributesGetter<REQUEST, RESPONSE> {
@Nullable
Double getRequestTopP(REQUEST request);

@Nullable
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We initially had discussion that it's confusing if a list can be empty or null, which was definitely correct for chat. But these semantic conventions are shared between chat / embeddings where ones like this one aren't present at all in embeddings, so I guess we should allow null for it

Copy link
Contributor

Choose a reason for hiding this comment

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

Since the GenAiAttributesExtractor treats null and empty the same way making this nullable isn't strictly necessary. @trask do you have a preference here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks - I reverted since indeed the behavior is the same

List<String> getResponseFinishReasons(REQUEST request, RESPONSE response);

@Nullable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.openai.v1_1;

import com.openai.client.OpenAIClient;
import com.openai.client.OpenAIClientAsync;
import io.opentelemetry.instrumentation.openai.v1_1.AbstractEmbeddingsTest;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.sdk.testing.assertj.SpanDataAssert;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.extension.RegisterExtension;

class EmbeddingsTest extends AbstractEmbeddingsTest {

@RegisterExtension
private static final AgentInstrumentationExtension testing =
AgentInstrumentationExtension.create();

@Override
protected InstrumentationExtension getTesting() {
return testing;
}

@Override
protected OpenAIClient wrap(OpenAIClient client) {
return client;
}

@Override
protected OpenAIClientAsync wrap(OpenAIClientAsync client) {
return client;
}

@Override
protected final List<Consumer<SpanDataAssert>> maybeWithTransportSpan(
Consumer<SpanDataAssert> span) {
List<Consumer<SpanDataAssert>> result = new ArrayList<>();
result.add(span);
// Do a very simple assertion since the telemetry is not part of this library.
result.add(s -> s.hasName("POST"));
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.openai.v1_1;

import static java.util.Collections.singletonList;

import com.openai.models.embeddings.CreateEmbeddingResponse;
import com.openai.models.embeddings.EmbeddingCreateParams;
import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesGetter;
import java.util.List;
import javax.annotation.Nullable;

enum EmbeddingAttributesGetter
implements GenAiAttributesGetter<EmbeddingCreateParams, CreateEmbeddingResponse> {
INSTANCE;

@Override
public String getOperationName(EmbeddingCreateParams request) {
return GenAiAttributes.GenAiOperationNameIncubatingValues.EMBEDDINGS;
}

@Override
public String getSystem(EmbeddingCreateParams request) {
return GenAiAttributes.GenAiSystemIncubatingValues.OPENAI;
}

@Override
public String getRequestModel(EmbeddingCreateParams request) {
return request.model().asString();
}

@Nullable
@Override
public Long getRequestSeed(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public List<String> getRequestEncodingFormats(EmbeddingCreateParams request) {
return request.encodingFormat().map(f -> singletonList(f.asString())).orElse(null);
}

@Nullable
@Override
public Double getRequestFrequencyPenalty(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public Long getRequestMaxTokens(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public Double getRequestPresencePenalty(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public List<String> getRequestStopSequences(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public Double getRequestTemperature(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public Double getRequestTopK(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public Double getRequestTopP(EmbeddingCreateParams request) {
return null;
}

@Nullable
@Override
public List<String> getResponseFinishReasons(
EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) {
return null;
}

@Nullable
@Override
public String getResponseId(
EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) {
return null;
}

@Nullable
@Override
public String getResponseModel(
EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) {
if (response == null) {
return null;
}
return response.model();
}

@Nullable
@Override
public Long getUsageInputTokens(
EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) {
if (response == null) {
return null;
}
return response.usage().promptTokens();
}

@Nullable
@Override
public Long getUsageOutputTokens(
EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class GenAiAttributes {

static final class GenAiOperationNameIncubatingValues {
static final String CHAT = "chat";
static final String EMBEDDINGS = "embeddings";

private GenAiOperationNameIncubatingValues() {}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.openai.v1_1;

import com.openai.core.RequestOptions;
import com.openai.models.embeddings.CreateEmbeddingResponse;
import com.openai.models.embeddings.EmbeddingCreateParams;
import com.openai.services.blocking.EmbeddingService;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import java.lang.reflect.Method;

final class InstrumentedEmbeddingService
extends DelegatingInvocationHandler<EmbeddingService, InstrumentedEmbeddingService> {

private final Instrumenter<EmbeddingCreateParams, CreateEmbeddingResponse> instrumenter;

public InstrumentedEmbeddingService(
EmbeddingService delegate,
Instrumenter<EmbeddingCreateParams, CreateEmbeddingResponse> instrumenter) {
super(delegate);
this.instrumenter = instrumenter;
}

@Override
protected Class<EmbeddingService> getProxyType() {
return EmbeddingService.class;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();

if (methodName.equals("create")
&& parameterTypes.length >= 1
&& parameterTypes[0] == EmbeddingCreateParams.class) {
if (parameterTypes.length == 1) {
return create((EmbeddingCreateParams) args[0], RequestOptions.none());
} else if (parameterTypes.length == 2 && parameterTypes[1] == RequestOptions.class) {
return create((EmbeddingCreateParams) args[0], (RequestOptions) args[1]);
}
}

return super.invoke(proxy, method, args);
}

private CreateEmbeddingResponse create(
EmbeddingCreateParams request, RequestOptions requestOptions) {
Context parentContext = Context.current();
if (!instrumenter.shouldStart(parentContext, request)) {
return delegate.create(request, requestOptions);
}

Context context = instrumenter.start(parentContext, request);
CreateEmbeddingResponse response;
try (Scope ignored = context.makeCurrent()) {
response = delegate.create(request, requestOptions);
} catch (Throwable t) {
instrumenter.end(context, request, null, t);
throw t;
}

instrumenter.end(context, request, response, null);
return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.openai.v1_1;

import com.openai.core.RequestOptions;
import com.openai.models.embeddings.CreateEmbeddingResponse;
import com.openai.models.embeddings.EmbeddingCreateParams;
import com.openai.services.async.EmbeddingServiceAsync;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import java.lang.reflect.Method;
import java.util.concurrent.CompletableFuture;

final class InstrumentedEmbeddingServiceAsync
extends DelegatingInvocationHandler<EmbeddingServiceAsync, InstrumentedEmbeddingServiceAsync> {

private final Instrumenter<EmbeddingCreateParams, CreateEmbeddingResponse> instrumenter;

public InstrumentedEmbeddingServiceAsync(
EmbeddingServiceAsync delegate,
Instrumenter<EmbeddingCreateParams, CreateEmbeddingResponse> instrumenter) {
super(delegate);
this.instrumenter = instrumenter;
}

@Override
protected Class<EmbeddingServiceAsync> getProxyType() {
return EmbeddingServiceAsync.class;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();

if (methodName.equals("create")
&& parameterTypes.length >= 1
&& parameterTypes[0] == EmbeddingCreateParams.class) {
if (parameterTypes.length == 1) {
return create((EmbeddingCreateParams) args[0], RequestOptions.none());
} else if (parameterTypes.length == 2 && parameterTypes[1] == RequestOptions.class) {
return create((EmbeddingCreateParams) args[0], (RequestOptions) args[1]);
}
}

return super.invoke(proxy, method, args);
}

private CompletableFuture<CreateEmbeddingResponse> create(
EmbeddingCreateParams request, RequestOptions requestOptions) {
Context parentContext = Context.current();
if (!instrumenter.shouldStart(parentContext, request)) {
return delegate.create(request, requestOptions);
}

Context context = instrumenter.start(parentContext, request);
CompletableFuture<CreateEmbeddingResponse> future;
try (Scope ignored = context.makeCurrent()) {
future = delegate.create(request, requestOptions);
} catch (Throwable t) {
instrumenter.end(context, request, null, t);
throw t;
}

future = future.whenComplete((res, t) -> instrumenter.end(context, request, res, t));
return CompletableFutureWrapper.wrap(future, parentContext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.openai.client.OpenAIClient;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.embeddings.CreateEmbeddingResponse;
import com.openai.models.embeddings.EmbeddingCreateParams;
import io.opentelemetry.api.logs.Logger;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import java.lang.reflect.Method;
Expand All @@ -16,16 +18,19 @@ final class InstrumentedOpenAiClient
extends DelegatingInvocationHandler<OpenAIClient, InstrumentedOpenAiClient> {

private final Instrumenter<ChatCompletionCreateParams, ChatCompletion> chatInstrumenter;
private final Instrumenter<EmbeddingCreateParams, CreateEmbeddingResponse> embeddingInstrumenter;
private final Logger eventLogger;
private final boolean captureMessageContent;

InstrumentedOpenAiClient(
OpenAIClient delegate,
Instrumenter<ChatCompletionCreateParams, ChatCompletion> chatInstrumenter,
Instrumenter<EmbeddingCreateParams, CreateEmbeddingResponse> embeddingInstrumenter,
Logger eventLogger,
boolean captureMessageContent) {
super(delegate);
this.chatInstrumenter = chatInstrumenter;
this.embeddingInstrumenter = embeddingInstrumenter;
this.eventLogger = eventLogger;
this.captureMessageContent = captureMessageContent;
}
Expand All @@ -44,9 +49,17 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
delegate.chat(), chatInstrumenter, eventLogger, captureMessageContent)
.createProxy();
}
if (methodName.equals("embeddings") && parameterTypes.length == 0) {
return new InstrumentedEmbeddingService(delegate.embeddings(), embeddingInstrumenter)
.createProxy();
}
if (methodName.equals("async") && parameterTypes.length == 0) {
return new InstrumentedOpenAiClientAsync(
delegate.async(), chatInstrumenter, eventLogger, captureMessageContent)
delegate.async(),
chatInstrumenter,
embeddingInstrumenter,
eventLogger,
captureMessageContent)
.createProxy();
}
return super.invoke(proxy, method, args);
Expand Down
Loading
Loading