chore: Move LangSmith OTel Java wrappers PoC into SDK

This commit is contained in:
xornivore
2025-11-07 15:13:27 -05:00
parent f7fb8ad3a7
commit 24680beb2f
12 changed files with 3123 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
plugins {
id("langchain.java")
id("langchain.publish")
}
// Suppress obsolete Java 8 warnings since we're using Java 21 toolchain but targeting Java 8
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-Xlint:-options")
}
// Customize POM for wrappers module
configure<com.vanniktech.maven.publish.MavenPublishBaseExtension> {
pom {
name.set("LangSmith Java Wrappers")
description.set("OpenTelemetry integration wrappers for LangSmith in Java. " +
"This package provides OpenTelemetry wrappers for AI model clients to enable " +
"automatic tracing and monitoring with LangSmith.")
}
}
dependencies {
// OpenAI Java SDK - Required for examples to run
// The wrappers wrap the OpenAI SDK generated by Stainless
// Users should add this dependency when using the wrappers in their own projects
// Example: implementation("com.openai:openai-java:4.6.1")
implementation("com.openai:openai-java:4.6.1")
// OpenTelemetry API
api("io.opentelemetry:opentelemetry-api:1.32.0")
api("io.opentelemetry:opentelemetry-context:1.32.0")
// OpenTelemetry SDK (for configuring exporters)
implementation("io.opentelemetry:opentelemetry-sdk:1.32.0")
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.32.0")
// Note: OtlpHttpSpanExporter should be in opentelemetry-exporter-otlp, but if not found,
// we may need to check the actual package structure
// Test dependencies
testImplementation("com.openai:openai-java:4.6.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
// Task to run examples
tasks.register<JavaExec>("runExample") {
group = "application"
description = "Run an example class"
classpath = sourceSets["main"].runtimeClasspath
// Get the example class from project property, or use default
val exampleClass = project.findProperty("exampleClass") as String?
mainClass.set(exampleClass ?: "com.langchain.smith.wrappers.openai.examples.SimpleChatCompletionExample")
// Pass through environment variables (especially OPENAI_API_KEY, LANGSMITH_API_KEY, etc.)
environment.putAll(System.getenv())
}
@@ -0,0 +1,54 @@
package com.langchain.smith.wrappers.openai;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
/**
* Utility class for wrapping OpenAI clients with LangSmith tracing
* capabilities.
*
* <p>
* This class provides a simple way to wrap OpenAI clients similar to the Python
* langsmith-sdk wrapper functionality.
*/
public final class OpenAIWrappers {
private OpenAIWrappers() {
// Utility class - prevent instantiation
}
/**
* Wraps an OpenAI client to add LangSmith tracing capabilities.
*
* <p>
* This is a no-op wrapper that maintains the same developer experience as the
* original client. All configuration options and methods work exactly as they
* would with the original client.
*
* @param client the OpenAI client to wrap
* @return a wrapped client that delegates to the original client
* @throws IllegalArgumentException if client is null
*/
public static WrappedOpenAIClient wrap(OpenAIClient client) {
if (client == null) {
throw new IllegalArgumentException("Client cannot be null");
}
return new WrappedOpenAIClient(client);
}
/**
* Creates a wrapped OpenAI client from environment variables.
*
* <p>
* This is equivalent to:
*
* <pre>{@code
* wrap(OpenAIOkHttpClient.fromEnv())
* }</pre>
*
* @return a wrapped OpenAI client configured from environment variables
*/
public static WrappedOpenAIClient wrapFromEnv() {
return wrap(OpenAIOkHttpClient.fromEnv());
}
}
@@ -0,0 +1,418 @@
package com.langchain.smith.wrappers.openai;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.SpanProcessor;
import java.util.concurrent.TimeUnit;
/**
* Configuration utility for setting up OpenTelemetry to export traces to
* LangSmith.
*
* <p>
* This class provides a convenient way to configure OpenTelemetry with
* LangSmith's OTLP
* endpoint. You can use this programmatically, or configure via environment
* variables.
*
* <p>
* Example usage:
*
* <pre>{@code
* // Configure OpenTelemetry for LangSmith before using the wrapper
* OpenTelemetryConfig.configureForLangSmith(
* "your-langsmith-api-key",
* "your-project-name");
*
* // Now use the wrapped client - traces will be sent to LangSmith
* WrappedOpenAIClient client = OpenAIWrappers.wrapFromEnv();
* }</pre>
*/
public final class OpenTelemetryConfig {
private OpenTelemetryConfig() {
// Utility class
}
/**
* LangSmith OTLP endpoint for traces.
* Can be overridden via LANGSMITH_OTLP_ENDPOINT environment variable or
* by passing a custom endpoint to the configuration methods.
*/
public static final String LANGSMITH_OTLP_ENDPOINT = "https://api.smith.langchain.com/otel/v1/traces";
/**
* Span processor type for configuring how spans are exported.
*/
public enum SpanProcessorType {
/**
* BatchSpanProcessor - Queues spans and exports them in batches.
* Best for production use - efficient and non-blocking.
*/
BATCH,
/**
* SimpleSpanProcessor - Exports spans synchronously on span.end().
* Best for testing - predictable but blocks application thread.
* WARNING: Not recommended for production due to performance impact.
*/
SIMPLE
}
/**
* Configures OpenTelemetry to export traces to LangSmith.
*
* <p>
* This method configures the global OpenTelemetry instance to send traces to
* LangSmith's
* OTLP endpoint. After calling this method, all spans created by the wrapped
* OpenAI client
* will be exported to LangSmith.
*
* @param apiKey your LangSmith API key
* @param projectName your LangSmith project name (optional, can be null)
* @return the configured OpenTelemetry instance
*/
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName) {
return configureForLangSmith(apiKey, projectName, null, null);
}
/**
* Configures OpenTelemetry to export traces to LangSmith with a custom service
* name.
*
* @param apiKey your LangSmith API key
* @param projectName your LangSmith project name (optional, can be null)
* @param serviceName the service name to identify your application (defaults to
* "langsmith-java-otel-wrappers")
* @return the configured OpenTelemetry instance
*/
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
String serviceName) {
return configureForLangSmith(apiKey, projectName, serviceName, null);
}
/**
* Configures OpenTelemetry to export traces to LangSmith with a custom service
* name and endpoint.
*
* @param apiKey your LangSmith API key
* @param projectName your LangSmith project name (optional, can be null)
* @param serviceName the service name to identify your application (defaults to
* "langsmith-java-otel-wrappers")
* @param endpoint the OTLP endpoint URL (optional, defaults to
* LANGSMITH_OTLP_ENDPOINT constant)
* @return the configured OpenTelemetry instance
*/
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
String serviceName, String endpoint) {
return configureForLangSmith(apiKey, projectName, serviceName, endpoint,
SpanProcessorType.BATCH, 512);
}
/**
* Configures OpenTelemetry to export traces to LangSmith with custom batch
* size.
*
* @param apiKey your LangSmith API key
* @param projectName your LangSmith project name (optional, can be null)
* @param serviceName the service name to identify your application (defaults
* to
* "langsmith-java-otel-wrappers")
* @param endpoint the OTLP endpoint URL (optional, defaults to
* LANGSMITH_OTLP_ENDPOINT constant)
* @param maxBatchSize the maximum batch size before export is triggered
* (set to 1 for immediate export, default 512)
* @return the configured OpenTelemetry instance
*/
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
String serviceName, String endpoint, int maxBatchSize) {
return configureForLangSmith(apiKey, projectName, serviceName, endpoint,
SpanProcessorType.BATCH, maxBatchSize);
}
/**
* Configures OpenTelemetry to export traces to LangSmith with custom span
* processor type and batch size.
*
* @param apiKey your LangSmith API key
* @param projectName your LangSmith project name (optional, can be null)
* @param serviceName the service name to identify your application
* (defaults to "langsmith-java-otel-wrappers")
* @param endpoint the OTLP endpoint URL (optional, defaults to
* LANGSMITH_OTLP_ENDPOINT constant)
* @param processorType the span processor type (BATCH or SIMPLE)
* @param maxBatchSize the maximum batch size before export is triggered
* (only used for BATCH processor, set to 1 for
* immediate export)
* @return the configured OpenTelemetry instance
*/
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
String serviceName, String endpoint, SpanProcessorType processorType, int maxBatchSize) {
if (apiKey == null || apiKey.isEmpty()) {
throw new IllegalArgumentException("LangSmith API key cannot be null or empty");
}
// Use provided endpoint or default to LANGSMITH_OTLP_ENDPOINT
String endpointUrl = endpoint != null && !endpoint.isEmpty()
? endpoint
: LANGSMITH_OTLP_ENDPOINT;
// Create OTLP HTTP exporter configured for LangSmith
// Build the exporter with conditional headers
// Note: Using Object to work around Java 8 limitation (can't use var)
// The builder() method returns a builder that supports method chaining
Object builder = OtlpHttpSpanExporter.builder()
.setEndpoint(endpointUrl)
.addHeader("x-api-key", apiKey);
// Only add project header if projectName is not null and not empty
if (projectName != null && !projectName.isEmpty()) {
// Use reflection to call addHeader on the builder
try {
java.lang.reflect.Method addHeaderMethod = builder.getClass().getMethod("addHeader", String.class,
String.class);
builder = addHeaderMethod.invoke(builder, "langsmith-project", projectName);
} catch (Exception e) {
throw new RuntimeException("Failed to add project header", e);
}
}
// Build the exporter using reflection
OtlpHttpSpanExporter spanExporter;
try {
java.lang.reflect.Method buildMethod = builder.getClass().getMethod("build");
spanExporter = (OtlpHttpSpanExporter) buildMethod.invoke(builder);
} catch (Exception e) {
throw new RuntimeException("Failed to build OtlpHttpSpanExporter", e);
}
// Wrap exporter to log export errors
SpanExporter loggingExporter = new LoggingSpanExporter(spanExporter);
// Create resource with service name
Resource resource = Resource.getDefault()
.merge(Resource.builder()
.put("service.name",
serviceName != null ? serviceName : "langsmith-java-otel-wrappers")
.build());
// Build and configure span processor based on type
SpanProcessor spanProcessor;
if (processorType == SpanProcessorType.SIMPLE) {
// SimpleSpanProcessor - exports synchronously on span.end()
// Good for testing, but blocks the application thread
spanProcessor = SimpleSpanProcessor.create(loggingExporter);
} else {
// BatchSpanProcessor - queues and exports in batches
// Good for production, non-blocking
// If maxBatchSize is 1, spans are exported immediately as they complete
spanProcessor = BatchSpanProcessor.builder(loggingExporter)
.setScheduleDelay(100, TimeUnit.MILLISECONDS) // Export every 100ms
.setMaxExportBatchSize(maxBatchSize) // Trigger export when batch reaches this size
.setExporterTimeout(5, TimeUnit.SECONDS) // 5 second timeout
.build();
}
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(spanProcessor)
.setResource(resource)
.build();
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.buildAndRegisterGlobal();
return openTelemetry;
}
/**
* Configures OpenTelemetry from environment variables.
*
* <p>
* Reads configuration from the following environment variables:
* <ul>
* <li>LANGSMITH_API_KEY - Required: Your LangSmith API key</li>
* <li>LANGSMITH_PROJECT - Optional: Your LangSmith project name</li>
* <li>OTEL_SERVICE_NAME - Optional: Service name (defaults to
* "langsmith-java-otel-wrappers")</li>
* <li>LANGSMITH_OTLP_ENDPOINT - Optional: Custom OTLP endpoint URL (defaults to
* LANGSMITH_OTLP_ENDPOINT constant)</li>
* </ul>
*
* @return the configured OpenTelemetry instance
* @throws IllegalStateException if LANGSMITH_API_KEY is not set
*/
public static OpenTelemetry configureFromEnv() {
String apiKey = System.getenv("LANGSMITH_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
throw new IllegalStateException(
"LANGSMITH_API_KEY environment variable is required. " +
"Please set it with your LangSmith API key.");
}
String projectName = System.getenv("LANGSMITH_PROJECT");
String serviceName = System.getenv("OTEL_SERVICE_NAME");
String endpoint = System.getenv("LANGSMITH_OTLP_ENDPOINT");
return configureForLangSmith(apiKey, projectName, serviceName, endpoint);
}
/**
* Forces flushing of all pending spans to ensure they are exported.
* This should be called before application shutdown to ensure all spans
* are sent to LangSmith.
*
* @return true if flush completed successfully, false otherwise
*/
public static boolean flush() {
return flush(5, TimeUnit.SECONDS);
}
/**
* Forces flushing of all pending spans to ensure they are exported.
* This should be called before application shutdown to ensure all spans
* are sent to LangSmith.
*
* @param timeout the maximum time to wait for flush to complete
* @param unit the time unit of the timeout
* @return true if flush completed successfully, false otherwise
*/
public static boolean flush(long timeout, TimeUnit unit) {
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
if (openTelemetry instanceof OpenTelemetrySdk) {
// Don't close the SDK instance - it's the global instance that should remain
// alive
@SuppressWarnings("resource")
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
if (tracerProvider != null) {
try {
io.opentelemetry.sdk.common.CompletableResultCode result = tracerProvider.forceFlush();
result.join(timeout, unit);
if (!result.isSuccess()) {
System.err.println("Warning: Flush did not complete successfully");
}
return result.isSuccess();
} catch (Exception e) {
System.err.println("Warning: Failed to flush spans: " + e.getMessage());
e.printStackTrace();
return false;
}
}
}
return true;
}
/**
* Shuts down the OpenTelemetry SDK and ensures all spans are exported.
* This should be called before application shutdown.
*/
public static void shutdown() {
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
if (openTelemetry instanceof OpenTelemetrySdk) {
// Don't close the SDK instance - it's the global instance that should remain
// alive
@SuppressWarnings("resource")
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
if (tracerProvider != null) {
try {
tracerProvider.shutdown().join(5, TimeUnit.SECONDS);
} catch (Exception e) {
System.err.println("Warning: Failed to shutdown OpenTelemetry: " + e.getMessage());
e.printStackTrace();
}
}
}
}
/**
* Wrapper SpanExporter that logs export errors to help debug issues.
*/
private static class LoggingSpanExporter implements SpanExporter {
private final SpanExporter delegate;
private static final boolean DEBUG = Boolean.getBoolean("langsmith.debug")
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
LoggingSpanExporter(SpanExporter delegate) {
this.delegate = delegate;
}
@Override
public io.opentelemetry.sdk.common.CompletableResultCode export(
java.util.Collection<io.opentelemetry.sdk.trace.data.SpanData> spans) {
if (DEBUG) {
System.out.println("[LangSmith] Exporting " + spans.size() + " span(s):");
for (io.opentelemetry.sdk.trace.data.SpanData span : spans) {
System.out.println(" - " + span.getName()
+ " (kind=" + span.getKind()
+ ", attributes=" + span.getAttributes().size() + ")");
}
}
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.export(spans);
// For SimpleSpanProcessor, wait for the result synchronously to get immediate
// feedback
// For BatchSpanProcessor, this will return immediately but we can still check
// status
if (DEBUG) {
// Wait up to 5 seconds for the result
try {
result.join(5, java.util.concurrent.TimeUnit.SECONDS);
if (!result.isSuccess()) {
System.err.println("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
System.err.println(" This usually indicates a network error or authentication problem");
System.err.println(" Check your LANGSMITH_API_KEY and network connectivity");
} else {
System.out.println("[LangSmith] Successfully exported " + spans.size() + " span(s)");
}
} catch (Exception e) {
System.err.println("[LangSmith ERROR] Exception waiting for export result: " + e.getMessage());
e.printStackTrace();
}
} else {
// Without DEBUG, still log errors but don't block
result.whenComplete(() -> {
if (!result.isSuccess()) {
System.err.println("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
System.err.println(" This usually indicates a network error or authentication problem");
System.err.println(" Check your LANGSMITH_API_KEY and network connectivity");
}
});
}
return result;
}
@Override
public io.opentelemetry.sdk.common.CompletableResultCode flush() {
if (DEBUG) {
System.out.println("[LangSmith] Flushing spans...");
}
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.flush();
result.whenComplete(() -> {
if (!result.isSuccess()) {
System.err.println("[LangSmith ERROR] Failed to flush spans");
} else if (DEBUG) {
System.out.println("[LangSmith] Flush completed successfully");
}
});
return result;
}
@Override
public io.opentelemetry.sdk.common.CompletableResultCode shutdown() {
if (DEBUG) {
System.out.println("[LangSmith] Shutting down span exporter...");
}
return delegate.shutdown();
}
}
}
@@ -0,0 +1,219 @@
package com.langchain.smith.wrappers.openai;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
/**
* Utility class for creating and managing OpenTelemetry spans for OpenAI API
* calls.
*
* <p>
* This class follows the LangSmith OTEL conventions documented in
* LANGSMITH_OTEL.md.
*/
final class TracingUtils {
private static final String INSTRUMENTATION_NAME = "langsmith-java-otel-wrappers";
private TracingUtils() {
// Utility class
}
/**
* Gets or creates a tracer for OpenAI operations.
*
* @return a tracer instance
*/
static Tracer getTracer() {
// Try to get the global tracer if available
try {
Tracer tracer = io.opentelemetry.api.GlobalOpenTelemetry.get()
.getTracer(INSTRUMENTATION_NAME);
// Debug: Check if tracer is a noop tracer
boolean debug = Boolean.getBoolean("langsmith.debug")
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
if (debug) {
// Check if the OpenTelemetry instance is a noop
io.opentelemetry.api.OpenTelemetry otel = io.opentelemetry.api.GlobalOpenTelemetry.get();
boolean isNoop = otel.getClass().getName().contains("Noop");
System.out.println("[TracingUtils] Tracer obtained: " + tracer.getClass().getName()
+ ", OpenTelemetry isNoop: " + isNoop);
}
return tracer;
} catch (Exception e) {
// Fall back to noop - GlobalOpenTelemetry.get() will return a noop
// implementation
// if OpenTelemetry is not configured
return io.opentelemetry.api.GlobalOpenTelemetry.get()
.getTracer(INSTRUMENTATION_NAME);
}
}
/**
* Creates a span builder for an OpenAI operation with LangSmith-specific
* attributes.
*
* @param model the model name (e.g., "gpt-4o-mini")
* @param operationType the operation type (e.g., "chat", "response")
* @param spanKind the LangSmith span kind (e.g., "llm") - can be null
* @return a span builder with CLIENT kind and core attributes
*/
static SpanBuilder createSpanBuilder(String model, String operationType, String spanKind) {
Tracer tracer = getTracer();
String spanName = operationType + " " + (model != null ? model : "unknown");
SpanBuilder builder = tracer.spanBuilder(spanName)
.setSpanKind(SpanKind.CLIENT)
.setAttribute("gen_ai.system", "openai")
.setAttribute("gen_ai.operation.name", operationType)
.setAttribute("gen_ai.provider.name", "openai");
// Set LangSmith span kind on the builder (important for LangSmith detection)
if (spanKind != null) {
builder.setAttribute("langsmith.span.kind", spanKind);
}
return builder;
}
/**
* Creates a span builder for an OpenAI operation (defaults to "llm" span kind).
*
* @param model the model name (e.g., "gpt-4o-mini")
* @param operationType the operation type (e.g., "chat", "response")
* @return a span builder with CLIENT kind and core attributes
*/
static SpanBuilder createSpanBuilder(String model, String operationType) {
return createSpanBuilder(model, operationType, "llm");
}
/**
* Sets common span attributes for OpenAI LLM requests.
*
* Note: Core attributes (gen_ai.system, gen_ai.operation.name,
* gen_ai.provider.name,
* langsmith.span.kind) are already set on the SpanBuilder. This method sets
* additional
* request-specific attributes.
*
* @param span the span to set attributes on
* @param model the model name
*/
static void setRequestAttributes(Span span, String model) {
if (model != null) {
span.setAttribute("gen_ai.request.model", model);
}
}
/**
* Sets request parameter attributes on a span.
*
* @param span the span to set attributes on
* @param temperature the temperature parameter (nullable)
* @param topP the top_p parameter (nullable)
* @param maxTokens the max_tokens parameter (nullable)
*/
static void setRequestParameters(Span span, Double temperature, Double topP, Long maxTokens) {
if (temperature != null) {
span.setAttribute("gen_ai.request.temperature", temperature);
}
if (topP != null) {
span.setAttribute("gen_ai.request.top_p", topP);
}
if (maxTokens != null) {
span.setAttribute("gen_ai.request.max_tokens", maxTokens);
}
}
/**
* Sets input messages as JSON array string.
*
* @param span the span to set attributes on
* @param messagesJson the messages in JSON array format
*/
static void setInputMessages(Span span, String messagesJson) {
if (messagesJson != null) {
span.setAttribute("gen_ai.input.messages", messagesJson);
}
}
/**
* Sets output messages as JSON array string.
*
* @param span the span to set attributes on
* @param messagesJson the messages in JSON array format
*/
static void setOutputMessages(Span span, String messagesJson) {
if (messagesJson != null) {
span.setAttribute("gen_ai.output.messages", messagesJson);
}
}
/**
* Sets response attributes on a span.
*
* @param span the span to set attributes on
* @param inputTokens number of input tokens
* @param outputTokens number of output tokens
* @param totalTokens total tokens used
*/
static void setResponseAttributes(Span span, Long inputTokens, Long outputTokens,
Long totalTokens) {
if (inputTokens != null) {
span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
}
if (outputTokens != null) {
span.setAttribute("gen_ai.usage.output_tokens", outputTokens);
}
if (totalTokens != null) {
span.setAttribute("gen_ai.usage.total_tokens", totalTokens);
}
}
/**
* Sets response model and finish reason attributes.
*
* @param span the span to set attributes on
* @param responseModel the model used in the response
* @param finishReason the finish reason
*/
static void setResponseMetadata(Span span, String responseModel, String finishReason) {
if (responseModel != null) {
span.setAttribute("gen_ai.response.model", responseModel);
}
if (finishReason != null) {
span.setAttribute("gen_ai.response.finish_reason", finishReason);
}
}
/**
* Records an exception on a span and marks it as an error.
*
* @param span the span to record the exception on
* @param exception the exception that occurred
*/
static void recordException(Span span, Throwable exception) {
span.recordException(exception);
span.setAttribute("error", true);
}
/**
* Escapes a JSON string by replacing special characters.
*
* @param str the string to escape
* @return the escaped string
*/
static String escapeJsonString(String str) {
if (str == null) {
return "";
}
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
@@ -0,0 +1,841 @@
package com.langchain.smith.wrappers.openai;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
import com.openai.models.chat.completions.StructuredChatCompletion;
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
import com.openai.core.RequestOptions;
import com.openai.core.http.StreamResponse;
import com.openai.models.chat.completions.ChatCompletionChunk;
import com.openai.services.blocking.ChatService;
import com.openai.services.blocking.chat.ChatCompletionService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import java.util.function.Consumer;
import com.openai.core.ClientOptions;
/**
* Wrapped ChatService that adds OpenTelemetry tracing to chat completion
* operations.
*/
class WrappedChatService implements ChatService {
private final ChatService delegate;
WrappedChatService(ChatService delegate) {
this.delegate = delegate;
}
@Override
public ChatService.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public ChatService withOptions(Consumer<ClientOptions.Builder> options) {
return new WrappedChatService(delegate.withOptions(options));
}
@Override
public ChatCompletionService completions() {
return new WrappedChatCompletionService(delegate.completions());
}
/**
* Wrapped ChatCompletionService that adds tracing to create operations.
*/
private static class WrappedChatCompletionService implements ChatCompletionService {
private final ChatCompletionService delegate;
WrappedChatCompletionService(ChatCompletionService delegate) {
this.delegate = delegate;
}
@Override
public ChatCompletionService.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public ChatCompletionService withOptions(Consumer<ClientOptions.Builder> options) {
return new WrappedChatCompletionService(delegate.withOptions(options));
}
@Override
public com.openai.services.blocking.chat.completions.MessageService messages() {
return delegate.messages();
}
@Override
public ChatCompletion create(ChatCompletionCreateParams params) {
return create(params, null);
}
@Override
public ChatCompletion create(ChatCompletionCreateParams params,
RequestOptions requestOptions) {
// Extract model from params for span naming
String model = params.model() != null ? params.model().toString() : "unknown";
Span span = TracingUtils.createSpanBuilder(model, "chat")
.startSpan();
// Debug: Check if span is recording (not a noop span)
boolean debug = Boolean.getBoolean("langsmith.debug")
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
if (debug) {
boolean isRecording = span.isRecording();
System.out.println("[WrappedChatService] Created span: " + span.getSpanContext().getSpanId()
+ ", isRecording: " + isRecording + ", traceId: " + span.getSpanContext().getTraceId());
}
try (Scope scope = span.makeCurrent()) {
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available (handle Optional types)
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
Long maxTokens = params.maxCompletionTokens().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
// Capture input messages in JSON format
String inputMessagesJson = formatInputMessages(params);
TracingUtils.setInputMessages(span, inputMessagesJson);
ChatCompletion result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// Extract response model and finish reason
String responseModel = result.model() != null ? result.model().toString() : null;
String finishReason = !result.choices().isEmpty() && result.choices().get(0).finishReason() != null
? result.choices().get(0).finishReason().toString()
: "stop";
TracingUtils.setResponseMetadata(span, responseModel, finishReason);
// Capture output messages in JSON format
String outputMessagesJson = formatOutputMessages(result);
TracingUtils.setOutputMessages(span, outputMessagesJson);
// Extract usage information from result
result.usage().ifPresent(usage -> {
TracingUtils.setResponseAttributes(span,
(long) usage.promptTokens(),
(long) usage.completionTokens(),
(long) usage.totalTokens());
});
// Create tool call spans for any tool calls in the response
createToolCallSpans(result, span);
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
if (debug) {
System.out.println("[WrappedChatService] Ending span: " + span.getSpanContext().getSpanId());
}
span.end();
if (debug) {
System.out.println("[WrappedChatService] Span ended: " + span.getSpanContext().getSpanId());
}
}
}
/**
* Formats input messages from ChatCompletionCreateParams as a JSON array
* string.
*
* <p>
* Properly formats messages as JSON with role and content, following
* LangSmith conventions.
*/
private String formatInputMessages(ChatCompletionCreateParams params) {
if (params.messages() == null || params.messages().isEmpty()) {
return "[]";
}
boolean debug = Boolean.getBoolean("langsmith.debug")
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
if (debug) {
System.out.println("[formatInputMessages] Processing " + params.messages().size() + " message(s)");
}
StringBuilder json = new StringBuilder("[");
boolean first = true;
for (com.openai.models.chat.completions.ChatCompletionMessageParam messageParam : params.messages()) {
if (!first) {
json.append(",");
}
first = false;
json.append("{");
String role = null;
String content = null;
// Try to extract role and content based on message type
// The OpenAI SDK uses different message types (UserMessage, SystemMessage,
// etc.)
String className = messageParam.getClass().getSimpleName();
String fullClassName = messageParam.getClass().getName();
// Debug logging (debug variable already declared above)
if (debug) {
System.out.println("[formatInputMessages] Processing message: " + fullClassName);
java.lang.reflect.Method[] allMethods = messageParam.getClass().getMethods();
System.out.println("[formatInputMessages] Available methods: " +
java.util.Arrays.stream(allMethods)
.map(m -> m.getName() + "(" + m.getParameterCount() + ")")
.collect(java.util.stream.Collectors.joining(", ")));
}
// ChatCompletionMessageParam is a union type - check type FIRST before calling
// as*() methods
// This prevents InvocationTargetException when calling asUser() on non-user
// messages
Object actualMessage = null;
try {
// Use type checking methods first to avoid exceptions
java.lang.reflect.Method isUserMethod = messageParam.getClass().getMethod("isUser");
java.lang.reflect.Method isSystemMethod = messageParam.getClass().getMethod("isSystem");
java.lang.reflect.Method isAssistantMethod = messageParam.getClass().getMethod("isAssistant");
java.lang.reflect.Method isToolMethod = messageParam.getClass().getMethod("isTool");
boolean isUser = (Boolean) isUserMethod.invoke(messageParam);
boolean isSystem = (Boolean) isSystemMethod.invoke(messageParam);
boolean isAssistant = (Boolean) isAssistantMethod.invoke(messageParam);
boolean isTool = (Boolean) isToolMethod.invoke(messageParam);
if (isUser) {
java.lang.reflect.Method asUserMethod = messageParam.getClass().getMethod("asUser");
actualMessage = asUserMethod.invoke(messageParam);
role = "user";
if (debug) {
System.out.println("[formatInputMessages] Found user message: " + actualMessage.getClass().getName());
}
} else if (isSystem) {
java.lang.reflect.Method asSystemMethod = messageParam.getClass().getMethod("asSystem");
actualMessage = asSystemMethod.invoke(messageParam);
role = "system";
if (debug) {
System.out.println("[formatInputMessages] Found system message: " + actualMessage.getClass().getName());
}
} else if (isAssistant) {
java.lang.reflect.Method asAssistantMethod = messageParam.getClass().getMethod("asAssistant");
actualMessage = asAssistantMethod.invoke(messageParam);
role = "assistant";
if (debug) {
System.out
.println("[formatInputMessages] Found assistant message: " + actualMessage.getClass().getName());
}
} else if (isTool) {
java.lang.reflect.Method asToolMethod = messageParam.getClass().getMethod("asTool");
actualMessage = asToolMethod.invoke(messageParam);
role = "tool";
if (debug) {
System.out.println("[formatInputMessages] Found tool message: " + actualMessage.getClass().getName());
}
}
// Now get content from the actual message object
if (actualMessage != null) {
try {
java.lang.reflect.Method contentMethod = actualMessage.getClass().getMethod("content");
Object contentResult = contentMethod.invoke(actualMessage);
if (debug) {
System.out.println("[formatInputMessages] content() returned: " +
(contentResult != null ? contentResult.getClass().getName() : "null"));
}
// Content might be a Content object with text() method
if (contentResult != null) {
try {
java.lang.reflect.Method textMethod = contentResult.getClass().getMethod("text");
Object textResult = textMethod.invoke(contentResult);
if (textResult instanceof java.util.Optional) {
@SuppressWarnings("unchecked")
java.util.Optional<String> textOpt = (java.util.Optional<String>) textResult;
if (textOpt.isPresent()) {
content = textOpt.get();
}
} else if (textResult instanceof String) {
content = (String) textResult;
}
} catch (NoSuchMethodException e) {
// Content might be a String directly
if (contentResult instanceof String) {
content = (String) contentResult;
} else if (contentResult instanceof java.util.Optional) {
@SuppressWarnings("unchecked")
java.util.Optional<String> contentOpt = (java.util.Optional<String>) contentResult;
if (contentOpt.isPresent()) {
content = contentOpt.get();
}
}
}
}
} catch (NoSuchMethodException e) {
if (debug) {
System.out.println("[formatInputMessages] No content() method on actual message");
}
}
}
} catch (Exception e) {
if (debug) {
System.out.println("[formatInputMessages] Error accessing message: " + e.getMessage());
e.printStackTrace();
}
}
// Role should already be set from asUser/asSystem/asAssistant above
// If not, try to get it from the message
if (role == null) {
try {
java.lang.reflect.Method roleMethod = messageParam.getClass().getMethod("role");
Object roleResult = roleMethod.invoke(messageParam);
if (roleResult != null) {
role = roleResult.toString().toLowerCase();
}
} catch (NoSuchMethodException e) {
// Fallback: determine role from class name
if (className.contains("User") || fullClassName.contains("User")) {
role = "user";
} else if (className.contains("System") || fullClassName.contains("System")) {
role = "system";
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
role = "assistant";
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
role = "tool";
}
} catch (Exception e) {
if (debug) {
System.out.println("[formatInputMessages] Error calling role(): " + e.getMessage());
}
// Fallback to class name
if (className.contains("User") || fullClassName.contains("User")) {
role = "user";
} else if (className.contains("System") || fullClassName.contains("System")) {
role = "system";
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
role = "assistant";
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
role = "tool";
}
}
}
// Final fallback: try toString parsing
if (content == null) {
String messageStr = messageParam.toString();
if (debug) {
System.out.println("[formatInputMessages] toString(): " + messageStr);
}
// For tool messages, look for content=Content{text={...}} pattern
if (role != null && role.equals("tool")) {
// Extract JSON content from tool messages:
// content=Content{text={"key":"value"}}
java.util.regex.Pattern toolContentPattern = java.util.regex.Pattern.compile("text=\\{([^}]+)\\}");
java.util.regex.Matcher toolMatcher = toolContentPattern.matcher(messageStr);
if (toolMatcher.find()) {
content = "{" + toolMatcher.group(1) + "}";
} else {
// Try simpler pattern: text="..."
java.util.regex.Pattern simplePattern = java.util.regex.Pattern.compile("text=[\"']([^\"']+)[\"']");
java.util.regex.Matcher simpleMatcher = simplePattern.matcher(messageStr);
if (simpleMatcher.find()) {
content = simpleMatcher.group(1);
}
}
}
// Improved extraction: try multiple patterns
if (content == null) {
// Pattern 1: content="..."
int contentIdx = messageStr.indexOf("content=");
if (contentIdx >= 0) {
// Look for Content{text="..."} pattern
int textIdx = messageStr.indexOf("text=", contentIdx);
if (textIdx > contentIdx) {
int start = messageStr.indexOf("\"", textIdx);
if (start >= 0) {
// Find matching closing quote, handling escaped quotes
int end = start + 1;
while (end < messageStr.length() && messageStr.charAt(end) != '"') {
if (messageStr.charAt(end) == '\\') {
end += 2; // Skip escaped character
} else {
end++;
}
}
if (end < messageStr.length()) {
content = messageStr.substring(start + 1, end);
}
}
}
// Fallback to simple quote matching
if (content == null) {
int start = messageStr.indexOf("\"", contentIdx);
if (start >= 0) {
int end = messageStr.indexOf("\"", start + 1);
if (end > start) {
content = messageStr.substring(start + 1, end);
}
} else {
// Try single quotes
start = messageStr.indexOf("'", contentIdx);
if (start >= 0) {
int end = messageStr.indexOf("'", start + 1);
if (end > start) {
content = messageStr.substring(start + 1, end);
}
}
}
}
}
}
// Pattern 2: Look for content in JSON-like format
if (content == null) {
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("content[=:]\\s*[\"']([^\"']+)[\"']");
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
if (matcher.find()) {
content = matcher.group(1);
}
}
// Pattern 3: If still null, try to extract from the end of the string
if (content == null && messageStr.length() > 0) {
// Last resort: try to find any quoted string that might be content
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[\"']([^\"']+)[\"']");
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
if (matcher.find()) {
String potentialContent = matcher.group(1);
// Only use if it looks like actual content (not a class name or role)
if (!potentialContent.contains("com.openai") &&
!potentialContent.equals("user") &&
!potentialContent.equals("system") &&
!potentialContent.equals("assistant") &&
!potentialContent.equals("tool") &&
potentialContent.length() > 0) {
content = potentialContent;
}
}
}
}
// Build JSON
if (role != null) {
json.append("\"role\":\"").append(role).append("\"");
}
if (content != null) {
if (role != null) {
json.append(",");
}
json.append("\"content\":\"")
.append(TracingUtils.escapeJsonString(content))
.append("\"");
}
if (debug) {
System.out.println("[formatInputMessages] Final: role=" + role + ", content="
+ (content != null ? content.substring(0, Math.min(50, content.length())) : "null"));
}
json.append("}");
}
json.append("]");
String result = json.toString();
if (debug) {
System.out.println("[formatInputMessages] Final JSON: " + result);
}
return result;
}
/**
* Creates tool call spans for any tool calls detected in the chat completion
* response.
*
* @param completion the chat completion response
* @param parentSpan the parent span (the chat completion span)
*/
private void createToolCallSpans(ChatCompletion completion, Span parentSpan) {
if (completion.choices() == null || completion.choices().isEmpty()) {
return;
}
boolean debug = Boolean.getBoolean("langsmith.debug")
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
// Check if message has tool calls
java.util.Optional<java.util.List<ChatCompletionMessageToolCall>> toolCallsOpt = message.toolCalls();
if (toolCallsOpt.isPresent()) {
java.util.List<ChatCompletionMessageToolCall> toolCalls = toolCallsOpt.get();
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
// Check if it's a function tool call
if (toolCall.isFunction()) {
ChatCompletionMessageFunctionToolCall functionToolCall = toolCall.asFunction();
createToolCallSpan(functionToolCall, parentSpan, debug);
}
// Note: Custom tool calls are not yet supported
}
}
}
}
/**
* Creates a single tool call span from a function tool call object.
*
* @param functionToolCall the function tool call object from the OpenAI SDK
* @param parentSpan the parent span (the chat completion span)
* @param debug whether debug logging is enabled
*/
private void createToolCallSpan(ChatCompletionMessageFunctionToolCall functionToolCall, Span parentSpan,
boolean debug) {
try {
io.opentelemetry.api.OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
io.opentelemetry.api.trace.Tracer tracer = openTelemetry.getTracer("langsmith-java-otel-wrappers");
// Extract tool call information directly from the SDK objects
String toolCallId = functionToolCall.id();
com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall.Function function = functionToolCall.function();
String toolName = function.name();
String toolArguments = function.arguments();
// Create span name
String spanName = toolName != null ? "tool_call " + toolName : "tool_call";
// Create tool call span as a child of the parent span
io.opentelemetry.api.trace.Span toolCallSpan = tracer.spanBuilder(spanName)
.setSpanKind(io.opentelemetry.api.trace.SpanKind.CLIENT)
.setAttribute("gen_ai.operation.name", "tool_call")
.setAttribute("langsmith.span.kind", "tool")
.setAttribute("gen_ai.system", "openai")
.setAttribute("gen_ai.provider.name", "openai")
.startSpan();
try (io.opentelemetry.context.Scope toolScope = toolCallSpan.makeCurrent()) {
// Set tool call attributes
if (toolCallId != null) {
toolCallSpan.setAttribute("gen_ai.tool.call.id", toolCallId);
}
if (toolName != null) {
toolCallSpan.setAttribute("gen_ai.tool.name", toolName);
toolCallSpan.setAttribute("langsmith.trace.name", "Tool Call: " + toolName);
}
if (toolArguments != null && !toolArguments.isEmpty()) {
toolCallSpan.setAttribute("gen_ai.tool.arguments", toolArguments);
}
if (debug) {
System.out
.println("[WrappedChatService] Created tool call span: " + toolCallSpan.getSpanContext().getSpanId()
+ ", tool=" + toolName + ", arguments=" + toolArguments + ", parent="
+ parentSpan.getSpanContext().getSpanId());
}
// Note: Tool call result would be set when the tool is actually executed
// This span represents the tool call request, not the execution
} finally {
toolCallSpan.end();
}
} catch (Exception e) {
if (debug) {
System.out.println("[WrappedChatService] Error creating tool call span: " + e.getMessage());
e.printStackTrace();
}
}
}
/**
* Formats output messages from ChatCompletion as a JSON array string.
*/
private String formatOutputMessages(ChatCompletion completion) {
if (completion.choices() == null || completion.choices().isEmpty()) {
return "[]";
}
StringBuilder json = new StringBuilder("[");
boolean first = true;
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
if (!first) {
json.append(",");
}
first = false;
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
json.append("{");
// Add role
json.append("\"role\":\"assistant\"");
// Add content if present
message.content().ifPresent(content -> {
json.append(",\"content\":\"")
.append(TracingUtils.escapeJsonString(content))
.append("\"");
});
json.append("}");
}
json.append("]");
return json.toString();
}
@Override
public <T> StructuredChatCompletion<T> create(
StructuredChatCompletionCreateParams<T> params) {
return create(params, null);
}
@Override
public <T> StructuredChatCompletion<T> create(
StructuredChatCompletionCreateParams<T> params, RequestOptions requestOptions) {
// Get model from the underlying params - StructuredChatCompletionCreateParams
// wraps ChatCompletionCreateParams
String model = params != null && params.rawParams() != null
&& params.rawParams().model() != null
? params.rawParams().model().toString()
: "unknown";
Span span = TracingUtils.createSpanBuilder(model, "chat")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available
if (params.rawParams() != null) {
Double temperature = params.rawParams().temperature().orElse(null);
Double topP = params.rawParams().topP().orElse(null);
Long maxTokens = params.rawParams().maxCompletionTokens().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
// Capture input messages in JSON format
String inputMessagesJson = formatInputMessages(params.rawParams());
TracingUtils.setInputMessages(span, inputMessagesJson);
}
StructuredChatCompletion<T> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// For structured completions, we'll just set basic model info
// The actual structured output is in the parsed result
TracingUtils.setResponseMetadata(span, model, null);
result.usage().ifPresent(usage -> {
TracingUtils.setResponseAttributes(span,
(long) usage.promptTokens(),
(long) usage.completionTokens(),
(long) usage.totalTokens());
});
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
@Override
public StreamResponse<ChatCompletionChunk> createStreaming(
ChatCompletionCreateParams params) {
return createStreaming(params, null);
}
@Override
public StreamResponse<ChatCompletionChunk> createStreaming(
ChatCompletionCreateParams params, RequestOptions requestOptions) {
String model = params.model() != null ? params.model().toString() : "unknown";
Span span = TracingUtils.createSpanBuilder(model, "chat")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
span.setAttribute("gen_ai.streaming", true);
// Set request parameters if available (handle Optional types)
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
Long maxTokens = params.maxCompletionTokens().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
// Capture input messages in JSON format
String inputMessagesJson = formatInputMessages(params);
TracingUtils.setInputMessages(span, inputMessagesJson);
// For streaming, we can't easily extract usage info, so we'll just mark it as
// streaming
StreamResponse<ChatCompletionChunk> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.createStreaming(params);
} else {
result = delegate.createStreaming(params, requestOptions);
}
// Note: For streaming, the span will end immediately
// This is a simplified implementation - in production you might want to
// wrap the stream to collect the full response
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
// Delegate other methods without tracing (for now)
@Override
public ChatCompletion retrieve(String completionId) {
return delegate.retrieve(completionId);
}
@Override
public ChatCompletion retrieve(String completionId,
com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
return delegate.retrieve(completionId, params);
}
@Override
public ChatCompletion retrieve(String completionId, RequestOptions requestOptions) {
return delegate.retrieve(completionId, requestOptions);
}
@Override
public ChatCompletion retrieve(String completionId,
com.openai.models.chat.completions.ChatCompletionRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieve(completionId, params, requestOptions);
}
@Override
public ChatCompletion retrieve(
com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
return delegate.retrieve(params);
}
@Override
public ChatCompletion retrieve(
com.openai.models.chat.completions.ChatCompletionRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieve(params, requestOptions);
}
@Override
public ChatCompletion update(String completionId,
com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
return delegate.update(completionId, params);
}
@Override
public ChatCompletion update(String completionId,
com.openai.models.chat.completions.ChatCompletionUpdateParams params,
RequestOptions requestOptions) {
return delegate.update(completionId, params, requestOptions);
}
@Override
public ChatCompletion update(
com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
return delegate.update(params);
}
@Override
public ChatCompletion update(
com.openai.models.chat.completions.ChatCompletionUpdateParams params,
RequestOptions requestOptions) {
return delegate.update(params, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list() {
return delegate.list();
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list(
RequestOptions requestOptions) {
return delegate.list(requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list(
com.openai.models.chat.completions.ChatCompletionListParams params) {
return delegate.list(params);
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list(
com.openai.models.chat.completions.ChatCompletionListParams params,
RequestOptions requestOptions) {
return delegate.list(params, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId) {
return delegate.delete(completionId);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId,
RequestOptions requestOptions) {
return delegate.delete(completionId, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId,
com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
return delegate.delete(completionId, params);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId,
com.openai.models.chat.completions.ChatCompletionDeleteParams params,
RequestOptions requestOptions) {
return delegate.delete(completionId, params, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
return delegate.delete(params);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
com.openai.models.chat.completions.ChatCompletionDeleteParams params,
RequestOptions requestOptions) {
return delegate.delete(params, requestOptions);
}
}
}
@@ -0,0 +1,277 @@
package com.langchain.smith.wrappers.openai;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import java.util.function.Consumer;
import com.openai.core.ClientOptions;
/**
* Wrapped OpenAI client that maintains the same developer experience as the
* original client
* while adding LangSmith tracing capabilities.
*
* <p>
* This wrapper delegates all calls to the underlying OpenAI client, allowing
* all
* configuration options and methods to work exactly as they would with the
* original client.
*/
public class WrappedOpenAIClient implements OpenAIClient {
private final OpenAIClient delegate;
/**
* Creates a new wrapped client that delegates to the provided client.
*
* @param delegate the underlying OpenAI client to wrap
*/
public WrappedOpenAIClient(OpenAIClient delegate) {
if (delegate == null) {
throw new IllegalArgumentException("Delegate client cannot be null");
}
this.delegate = delegate;
}
/**
* Gets the underlying delegate client.
*
* @return the wrapped OpenAI client
*/
public OpenAIClient getDelegate() {
return delegate;
}
@Override
public com.openai.client.OpenAIClientAsync async() {
return delegate.async();
}
@Override
public OpenAIClient.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public OpenAIClient withOptions(Consumer<ClientOptions.Builder> options) {
return delegate.withOptions(options);
}
@Override
public com.openai.services.blocking.CompletionService completions() {
return delegate.completions();
}
@Override
public com.openai.services.blocking.ChatService chat() {
return new WrappedChatService(delegate.chat());
}
@Override
public com.openai.services.blocking.EmbeddingService embeddings() {
return delegate.embeddings();
}
@Override
public com.openai.services.blocking.FileService files() {
return delegate.files();
}
@Override
public com.openai.services.blocking.ImageService images() {
return delegate.images();
}
@Override
public com.openai.services.blocking.AudioService audio() {
return delegate.audio();
}
@Override
public com.openai.services.blocking.ModerationService moderations() {
return delegate.moderations();
}
@Override
public com.openai.services.blocking.ModelService models() {
return delegate.models();
}
@Override
public com.openai.services.blocking.FineTuningService fineTuning() {
return delegate.fineTuning();
}
@Override
public com.openai.services.blocking.GraderService graders() {
return delegate.graders();
}
@Override
public com.openai.services.blocking.VectorStoreService vectorStores() {
return delegate.vectorStores();
}
@Override
public com.openai.services.blocking.WebhookService webhooks() {
return delegate.webhooks();
}
@Override
public com.openai.services.blocking.BetaService beta() {
return delegate.beta();
}
@Override
public com.openai.services.blocking.BatchService batches() {
return delegate.batches();
}
@Override
public com.openai.services.blocking.UploadService uploads() {
return delegate.uploads();
}
@Override
public com.openai.services.blocking.ResponseService responses() {
return new WrappedResponseService(delegate.responses());
}
@Override
public com.openai.services.blocking.RealtimeService realtime() {
return delegate.realtime();
}
@Override
public com.openai.services.blocking.ConversationService conversations() {
return delegate.conversations();
}
@Override
public com.openai.services.blocking.EvalService evals() {
return delegate.evals();
}
@Override
public com.openai.services.blocking.ContainerService containers() {
return delegate.containers();
}
@Override
public com.openai.services.blocking.VideoService videos() {
return delegate.videos();
}
@Override
public void close() {
delegate.close();
}
/**
* Builder for creating wrapped OpenAI clients with the same configuration
* options
* as the original client builder.
*/
public static class Builder {
private final OpenAIOkHttpClient.Builder delegateBuilder;
/**
* Creates a new builder that wraps the OpenAI client builder.
*/
public Builder() {
this.delegateBuilder = OpenAIOkHttpClient.builder();
}
/**
* Creates a new builder that wraps the OpenAI client builder, starting from
* environment variables.
*
* @return this builder for method chaining
*/
public Builder fromEnv() {
delegateBuilder.fromEnv();
return this;
}
/**
* Sets the API key.
*
* @param apiKey the OpenAI API key
* @return this builder for method chaining
*/
public Builder apiKey(String apiKey) {
delegateBuilder.apiKey(apiKey);
return this;
}
/**
* Sets the organization ID.
*
* @param organization the organization ID
* @return this builder for method chaining
*/
public Builder organization(String organization) {
delegateBuilder.organization(organization);
return this;
}
/**
* Sets the project ID.
*
* @param project the project ID
* @return this builder for method chaining
*/
public Builder project(String project) {
delegateBuilder.project(project);
return this;
}
/**
* Sets the webhook secret.
*
* @param webhookSecret the webhook secret
* @return this builder for method chaining
*/
public Builder webhookSecret(String webhookSecret) {
delegateBuilder.webhookSecret(webhookSecret);
return this;
}
/**
* Sets the base URL.
*
* @param baseUrl the base URL
* @return this builder for method chaining
*/
public Builder baseUrl(String baseUrl) {
delegateBuilder.baseUrl(baseUrl);
return this;
}
/**
* Builds the wrapped OpenAI client.
*
* @return a new wrapped OpenAI client
*/
public WrappedOpenAIClient build() {
return new WrappedOpenAIClient(delegateBuilder.build());
}
}
/**
* Creates a new builder for constructing wrapped OpenAI clients.
*
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates a wrapped OpenAI client from environment variables.
*
* @return a new wrapped OpenAI client configured from environment variables
*/
public static WrappedOpenAIClient fromEnv() {
return builder().fromEnv().build();
}
}
@@ -0,0 +1,414 @@
package com.langchain.smith.wrappers.openai;
import com.openai.models.responses.Response;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.StructuredResponse;
import com.openai.models.responses.StructuredResponseCreateParams;
import com.openai.core.RequestOptions;
import com.openai.core.http.StreamResponse;
import com.openai.models.responses.ResponseStreamEvent;
import com.openai.services.blocking.ResponseService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import java.util.function.Consumer;
import com.openai.core.ClientOptions;
/**
* Wrapped ResponseService that adds OpenTelemetry tracing to response operations.
*/
class WrappedResponseService implements ResponseService {
private final ResponseService delegate;
WrappedResponseService(ResponseService delegate) {
this.delegate = delegate;
}
@Override
public ResponseService.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public ResponseService withOptions(Consumer<ClientOptions.Builder> options) {
return new WrappedResponseService(delegate.withOptions(options));
}
@Override
public com.openai.services.blocking.responses.InputItemService inputItems() {
return delegate.inputItems();
}
@Override
public com.openai.services.blocking.responses.InputTokenService inputTokens() {
return delegate.inputTokens();
}
@Override
public Response create() {
return create((ResponseCreateParams) null, null);
}
@Override
public Response create(RequestOptions requestOptions) {
return create((ResponseCreateParams) null, requestOptions);
}
@Override
public Response create(ResponseCreateParams params) {
return create(params, null);
}
@Override
public Response create(ResponseCreateParams params, RequestOptions requestOptions) {
// Extract model from params
String model = params != null && params.model() != null
? params.model().toString()
: "unknown";
Span span = TracingUtils.createSpanBuilder(model, "response")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available (handle Optional types)
if (params != null) {
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
Response result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// Extract response model (simplified - just use the request model)
TracingUtils.setResponseMetadata(span, model, null);
// Extract usage information from result
if (result.usage().isPresent()) {
com.openai.models.responses.ResponseUsage usage = result.usage().get();
TracingUtils.setResponseAttributes(span,
(long) usage.inputTokens(),
(long) usage.outputTokens(),
(long) usage.totalTokens());
}
if (result.status() != null) {
span.setAttribute("gen_ai.response.status", result.status().toString());
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
@Override
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params) {
return create(params, null);
}
@Override
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params,
RequestOptions requestOptions) {
// Get model from rawParams
String model = params != null && params.rawParams() != null
&& params.rawParams().model() != null
? params.rawParams().model().toString()
: "unknown";
Span span = TracingUtils.createSpanBuilder(model, "response")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available (handle Optional types)
if (params != null && params.rawParams() != null) {
Double temperature = params.rawParams().temperature().orElse(null);
Double topP = params.rawParams().topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
StructuredResponse<T> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// Extract response model (simplified - just use the request model)
TracingUtils.setResponseMetadata(span, model, null);
if (result.usage().isPresent()) {
com.openai.models.responses.ResponseUsage usage = result.usage().get();
TracingUtils.setResponseAttributes(span,
(long) usage.inputTokens(),
(long) usage.outputTokens(),
(long) usage.totalTokens());
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming() {
return createStreaming((ResponseCreateParams) null, null);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(RequestOptions requestOptions) {
return createStreaming((ResponseCreateParams) null, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(ResponseCreateParams params) {
return createStreaming(params, null);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(ResponseCreateParams params,
RequestOptions requestOptions) {
String model = params != null && params.model() != null
? params.model().toString()
: "unknown";
Span span = TracingUtils.createSpanBuilder(model, "response")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
span.setAttribute("gen_ai.streaming", true);
// Set request parameters if available (handle Optional types)
if (params != null) {
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
StreamResponse<ResponseStreamEvent> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.createStreaming(params);
} else {
result = delegate.createStreaming(params, requestOptions);
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
// Note: For streaming, the span will end immediately
span.end();
}
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(
StructuredResponseCreateParams<?> params) {
return createStreaming(params, null);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(
StructuredResponseCreateParams<?> params, RequestOptions requestOptions) {
// Get model from rawParams
String model = params != null && params.rawParams() != null
&& params.rawParams().model() != null
? params.rawParams().model().toString()
: "unknown";
Span span = TracingUtils.createSpanBuilder(model, "response")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
span.setAttribute("gen_ai.streaming", true);
// Set request parameters if available (handle Optional types)
if (params != null && params.rawParams() != null) {
Double temperature = params.rawParams().temperature().orElse(null);
Double topP = params.rawParams().topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
StreamResponse<ResponseStreamEvent> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.createStreaming(params);
} else {
result = delegate.createStreaming(params, requestOptions);
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
// Delegate other methods without tracing (for now)
@Override
public Response retrieve(String responseId) {
return delegate.retrieve(responseId);
}
@Override
public Response retrieve(String responseId, RequestOptions requestOptions) {
return delegate.retrieve(responseId, requestOptions);
}
@Override
public Response retrieve(String responseId,
com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieve(responseId, params);
}
@Override
public Response retrieve(String responseId,
com.openai.models.responses.ResponseRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieve(responseId, params, requestOptions);
}
@Override
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieve(params);
}
@Override
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieve(params, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId) {
return delegate.retrieveStreaming(responseId);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId,
RequestOptions requestOptions) {
return delegate.retrieveStreaming(responseId, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId,
com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieveStreaming(responseId, params);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId,
com.openai.models.responses.ResponseRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieveStreaming(responseId, params, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieveStreaming(params);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
com.openai.models.responses.ResponseRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieveStreaming(params, requestOptions);
}
@Override
public void delete(String responseId) {
delegate.delete(responseId);
}
@Override
public void delete(String responseId, RequestOptions requestOptions) {
delegate.delete(responseId, requestOptions);
}
@Override
public void delete(String responseId,
com.openai.models.responses.ResponseDeleteParams params) {
delegate.delete(responseId, params);
}
@Override
public void delete(String responseId,
com.openai.models.responses.ResponseDeleteParams params,
RequestOptions requestOptions) {
delegate.delete(responseId, params, requestOptions);
}
@Override
public void delete(com.openai.models.responses.ResponseDeleteParams params) {
delegate.delete(params);
}
@Override
public void delete(com.openai.models.responses.ResponseDeleteParams params,
RequestOptions requestOptions) {
delegate.delete(params, requestOptions);
}
@Override
public Response cancel(String responseId) {
return delegate.cancel(responseId);
}
@Override
public Response cancel(String responseId, RequestOptions requestOptions) {
return delegate.cancel(responseId, requestOptions);
}
@Override
public Response cancel(String responseId,
com.openai.models.responses.ResponseCancelParams params) {
return delegate.cancel(responseId, params);
}
@Override
public Response cancel(String responseId,
com.openai.models.responses.ResponseCancelParams params,
RequestOptions requestOptions) {
return delegate.cancel(responseId, params, requestOptions);
}
@Override
public Response cancel(com.openai.models.responses.ResponseCancelParams params) {
return delegate.cancel(params);
}
@Override
public Response cancel(com.openai.models.responses.ResponseCancelParams params,
RequestOptions requestOptions) {
return delegate.cancel(params, requestOptions);
}
}
@@ -0,0 +1,446 @@
package com.langchain.smith.wrappers.openai.examples;
import com.langchain.smith.wrappers.openai.OpenTelemetryConfig;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.ChatModel;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
/**
* Example demonstrating manual OpenTelemetry span creation for LangSmith
* tracing.
*
* <p>
* This example shows how to manually create spans with proper gen_ai attributes
* that will be correctly interpreted by LangSmith. It demonstrates:
* <ul>
* <li>Single span LLM invocation wrapped in a manually created span</li>
* <li>Nested spans with parent-child relationships (root with two
* children)</li>
* </ul>
*
* <p>
* The spans created here follow the LangSmith OTEL conventions documented in
* LANGSMITH_OTEL.md, ensuring proper mapping to LangSmith runs.
*/
public class ManualSpansExample {
private static final String INSTRUMENTATION_NAME = "langsmith-java-otel-wrappers";
/**
* Main method demonstrating manual span creation.
*
* <p>
* Prerequisites:
* <ol>
* <li>Set OPENAI_API_KEY environment variable</li>
* <li>Set LANGSMITH_API_KEY environment variable (your LangSmith API key)</li>
* <li>Optionally set LANGSMITH_PROJECT environment variable</li>
* </ol>
*
* @param args command line arguments (not used)
*/
public static void main(String[] args) {
// Configure OpenTelemetry for LangSmith with batch size 1 for immediate export
// This eliminates the need for sleep delays before flush
System.out.println("Configuring OpenTelemetry for LangSmith...");
try {
String apiKey = System.getenv("LANGSMITH_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
throw new IllegalStateException("LANGSMITH_API_KEY environment variable is required");
}
String projectName = System.getenv("LANGSMITH_PROJECT");
String serviceName = System.getenv("OTEL_SERVICE_NAME");
String endpoint = System.getenv("LANGSMITH_OTLP_ENDPOINT");
// Option 1: Use SimpleSpanProcessor for synchronous, immediate export
// This ensures spans are sent immediately without buffering
// SimpleSpanProcessor blocks on span.end() until export completes
OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName, endpoint,
OpenTelemetryConfig.SpanProcessorType.SIMPLE, 1);
System.out.println("✓ OpenTelemetry configured with SimpleSpanProcessor (immediate export)\n");
// Option 2: Use BatchSpanProcessor with batch size = 1 for non-blocking
// immediate export
// Uncomment this line and comment Option 1 above to test BatchSpanProcessor
// This is non-blocking but still exports immediately due to batch size = 1
// OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName,
// endpoint,
// OpenTelemetryConfig.SpanProcessorType.BATCH, 1);
// System.out.println("✓ OpenTelemetry configured with BatchSpanProcessor
// (batch size = 1)\n");
} catch (IllegalStateException e) {
System.err.println("✗ Error configuring OpenTelemetry: " + e.getMessage());
System.err.println("\nPlease set the following environment variables:");
System.err.println(" - LANGSMITH_API_KEY (required)");
System.err.println(" - LANGSMITH_PROJECT (optional)");
return;
}
// Get OpenAI API key
String openaiApiKey = System.getenv("OPENAI_API_KEY");
if (openaiApiKey == null || openaiApiKey.isEmpty()) {
System.err.println("✗ OPENAI_API_KEY environment variable is required");
return;
}
// Create OpenAI client (not wrapped - we'll create spans manually)
OpenAIClient client = OpenAIOkHttpClient.builder()
.apiKey(openaiApiKey)
.build();
try {
// Example 1: Single span LLM invocation
System.out.println(repeatString("=", 60));
System.out.println("Example 1: Single Span LLM Invocation");
System.out.println(repeatString("=", 60));
exampleSingleSpan(client);
// Example 2: Nested spans (root with two children)
System.out.println("\n" + repeatString("=", 60));
System.out.println("Example 2: Nested Spans (Root with Two Children)");
System.out.println(repeatString("=", 60));
exampleNestedSpans(client);
} catch (Exception e) {
System.err.println("✗ Error during execution: " + e.getMessage());
e.printStackTrace();
} finally {
// Close the client
client.close();
System.out.println("\n" + repeatString("=", 60));
boolean flushed = OpenTelemetryConfig.flush(10, java.util.concurrent.TimeUnit.SECONDS);
if (!flushed) {
System.err.println("✗ Warning: Flush did not complete successfully");
System.err.println(" Some spans may not have been exported to LangSmith");
} else {
System.out.println("✓ Spans flushed successfully");
}
System.out.println("\n✓ Check your LangSmith dashboard to see the traces!");
}
}
/**
* Example 1: Single span LLM invocation.
*
* <p>
* Creates a single span wrapping an OpenAI chat completion call with all
* required gen_ai attributes according to LangSmith conventions.
*/
private static void exampleSingleSpan(OpenAIClient client) {
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME);
// Create span builder for LLM operation
Span span = tracer.spanBuilder("chat gpt-4o-mini")
.setSpanKind(SpanKind.CLIENT)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Set core gen_ai attributes for LLM type detection
span.setAttribute("gen_ai.system", "openai");
span.setAttribute("gen_ai.operation.name", "chat");
span.setAttribute("gen_ai.provider.name", "openai");
// Create chat completion request
String userMessage = "What is the capital of France?";
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.addUserMessage(userMessage)
.temperature(0.7)
.build();
// Set request attributes
span.setAttribute("gen_ai.request.model", "gpt-4o-mini");
span.setAttribute("gen_ai.request.temperature", 0.7);
// Set input messages as JSON array (following LangSmith format)
String inputMessagesJson = String.format(
"[{\"role\":\"user\",\"content\":\"%s\"}]",
escapeJsonString(userMessage));
span.setAttribute("gen_ai.input.messages", inputMessagesJson);
System.out.println("Making OpenAI API call...");
// Make the actual API call
ChatCompletion completion = client.chat().completions().create(params);
// Extract response data
String assistantContent = completion.choices().get(0).message().content()
.orElse("No content");
String finishReason = completion.choices().get(0).finishReason() != null
? completion.choices().get(0).finishReason().toString()
: "stop";
// Set response attributes
String responseModel = completion.model().toString();
span.setAttribute("gen_ai.response.model", responseModel);
span.setAttribute("gen_ai.response.finish_reason", finishReason);
// Set output messages as JSON array
String outputMessagesJson = String.format(
"[{\"role\":\"assistant\",\"content\":\"%s\"}]",
escapeJsonString(assistantContent));
span.setAttribute("gen_ai.output.messages", outputMessagesJson);
// Set token usage attributes
completion.usage().ifPresent(usage -> {
span.setAttribute("gen_ai.usage.input_tokens", (long) usage.promptTokens());
span.setAttribute("gen_ai.usage.output_tokens", (long) usage.completionTokens());
span.setAttribute("gen_ai.usage.total_tokens", (long) usage.totalTokens());
});
System.out.println("Response: " + assistantContent);
completion.usage().ifPresent(usage -> {
System.out.println("Tokens - Input: " + usage.promptTokens()
+ ", Output: " + usage.completionTokens()
+ ", Total: " + usage.totalTokens());
});
System.out.println("✓ Single span created successfully");
} catch (Exception e) {
// Record exception on span
span.recordException(e);
span.setAttribute("error", true);
throw e;
} finally {
span.end();
}
}
/**
* Example 2: Nested spans (root with two children).
*
* <p>
* Creates a root span representing a workflow/chain, with two child spans
* representing individual LLM calls. This demonstrates parent-child
* relationships in LangSmith.
*/
private static void exampleNestedSpans(OpenAIClient client) {
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME);
// Create root span (chain/workflow)
Span rootSpan = tracer.spanBuilder("nested_spans_chain")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute("gen_ai.operation.name", "nested_spans")
.setAttribute("langsmith.span.kind", "chain")
.setAttribute("langsmith.trace.name", "nested_spans_chain_" + System.currentTimeMillis())
.startSpan();
try (Scope rootScope = rootSpan.makeCurrent()) {
// Set attributes for root span (chain type)
System.out.println("Creating root span: nested_spans_chain");
System.out.println("Root Span ID: " + rootSpan.getSpanContext().getSpanId());
System.out.println("Root Trace ID: " + rootSpan.getSpanContext().getTraceId());
// Child span 1: First LLM call
System.out.println("\n--- Child Span 1: First Query ---");
Span childSpan1 = tracer.spanBuilder("chat gpt-4o-mini (step 1)")
.setSpanKind(SpanKind.CLIENT)
.setAttribute("gen_ai.operation.name", "chat")
.setAttribute("langsmith.span.kind", "llm")
.setAttribute("langsmith.trace.name", "First LLM Call")
.startSpan();
try (Scope childScope1 = childSpan1.makeCurrent()) {
// Set LLM attributes for child span 1
childSpan1.setAttribute("gen_ai.system", "openai");
childSpan1.setAttribute("gen_ai.operation.name", "chat");
childSpan1.setAttribute("gen_ai.provider.name", "openai");
childSpan1.setAttribute("gen_ai.request.model", "gpt-4o-mini");
childSpan1.setAttribute("gen_ai.request.temperature", 0.7);
String query1 = "What is the capital of France?";
ChatCompletionCreateParams params1 = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.addUserMessage(query1)
.temperature(0.7)
.build();
String inputMessages1 = String.format(
"[{\"role\":\"user\",\"content\":\"%s\"}]",
escapeJsonString(query1));
childSpan1.setAttribute("gen_ai.input.messages", inputMessages1);
ChatCompletion completion1 = client.chat().completions().create(params1);
String response1 = completion1.choices().get(0).message().content()
.orElse("No content");
childSpan1.setAttribute("gen_ai.response.model", completion1.model().toString());
String outputMessages1 = String.format(
"[{\"role\":\"assistant\",\"content\":\"%s\"}]",
escapeJsonString(response1));
childSpan1.setAttribute("gen_ai.output.messages", outputMessages1);
completion1.usage().ifPresent(usage -> {
childSpan1.setAttribute("gen_ai.usage.input_tokens", (long) usage.promptTokens());
childSpan1.setAttribute("gen_ai.usage.output_tokens", (long) usage.completionTokens());
childSpan1.setAttribute("gen_ai.usage.total_tokens", (long) usage.totalTokens());
});
System.out.println("Query 1: " + query1);
System.out.println("Response 1: " + response1);
System.out.println("Child Span 1 ID: " + childSpan1.getSpanContext().getSpanId());
System.out.println("Child Span 1 Parent: " + rootSpan.getSpanContext().getSpanId());
} catch (Exception e) {
childSpan1.recordException(e);
childSpan1.setAttribute("error", true);
throw e;
} finally {
childSpan1.end();
}
// Child span 2: Second LLM call
System.out.println("\n--- Child Span 2: Follow-up Query ---");
Span childSpan2 = tracer.spanBuilder("chat gpt-4o-mini (step 2)")
.setSpanKind(SpanKind.CLIENT)
.setAttribute("gen_ai.operation.name", "chat")
.setAttribute("langsmith.span.kind", "llm")
.setAttribute("langsmith.trace.name", "Second LLM Call")
.startSpan();
try (Scope childScope2 = childSpan2.makeCurrent()) {
// Set LLM attributes for child span 2
childSpan2.setAttribute("gen_ai.system", "openai");
childSpan2.setAttribute("gen_ai.operation.name", "chat");
childSpan2.setAttribute("gen_ai.provider.name", "openai");
childSpan2.setAttribute("gen_ai.request.model", "gpt-4o-mini");
childSpan2.setAttribute("gen_ai.request.temperature", 0.7);
String query2 = "What is the population of that city?";
ChatCompletionCreateParams params2 = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.addUserMessage(query2)
.temperature(0.7)
.build();
String inputMessages2 = String.format(
"[{\"role\":\"user\",\"content\":\"%s\"}]",
escapeJsonString(query2));
childSpan2.setAttribute("gen_ai.input.messages", inputMessages2);
ChatCompletion completion2 = client.chat().completions().create(params2);
String response2 = completion2.choices().get(0).message().content()
.orElse("No content");
childSpan2.setAttribute("gen_ai.response.model", completion2.model().toString());
String outputMessages2 = String.format(
"[{\"role\":\"assistant\",\"content\":\"%s\"}]",
escapeJsonString(response2));
childSpan2.setAttribute("gen_ai.output.messages", outputMessages2);
completion2.usage().ifPresent(usage -> {
childSpan2.setAttribute("gen_ai.usage.input_tokens", (long) usage.promptTokens());
childSpan2.setAttribute("gen_ai.usage.output_tokens", (long) usage.completionTokens());
childSpan2.setAttribute("gen_ai.usage.total_tokens", (long) usage.totalTokens());
});
System.out.println("Query 2: " + query2);
System.out.println("Response 2: " + response2);
System.out.println("Child Span 2 ID: " + childSpan2.getSpanContext().getSpanId());
System.out.println("Child Span 2 Parent: " + rootSpan.getSpanContext().getSpanId());
} catch (Exception e) {
childSpan2.recordException(e);
childSpan2.setAttribute("error", true);
throw e;
} finally {
childSpan2.end();
}
// Child span 3: Tool call
System.out.println("\n--- Child Span 3: Tool call ---");
Span childSpan3 = tracer.spanBuilder("tool_call")
.setSpanKind(SpanKind.CLIENT)
.setAttribute("gen_ai.operation.name", "tool_call")
.setAttribute("langsmith.span.kind", "tool")
.setAttribute("langsmith.trace.name", "Tool Call")
.startSpan();
try (Scope childScope3 = childSpan3.makeCurrent()) {
// Set tool call attributes for child span 3
childSpan3.setAttribute("gen_ai.system", "openai");
childSpan3.setAttribute("gen_ai.operation.name", "tool_call");
childSpan3.setAttribute("gen_ai.provider.name", "openai");
childSpan3.setAttribute("gen_ai.request.model", "gpt-4o-mini");
childSpan3.setAttribute("gen_ai.request.temperature", 0.7);
String toolName = "get_weather";
String toolDescription = "Get the weather for a given city";
String toolArguments = "{\"city\":\"Paris\"}";
childSpan3.setAttribute("gen_ai.tool.name", toolName);
childSpan3.setAttribute("gen_ai.tool.description", toolDescription);
childSpan3.setAttribute("gen_ai.tool.arguments", toolArguments);
System.out.println("Tool Name: " + toolName);
System.out.println("Tool Description: " + toolDescription);
System.out.println("Tool Arguments: " + toolArguments);
System.out.println("Child Span 3 ID: " + childSpan3.getSpanContext().getSpanId());
System.out.println("Child Span 3 Parent: " + rootSpan.getSpanContext().getSpanId());
} catch (Exception e) {
childSpan3.recordException(e);
childSpan3.setAttribute("error", true);
throw e;
} finally {
childSpan3.end();
}
System.out.println("\n✓ Nested spans created successfully");
System.out.println(" - Root span: multi-step-query (chain)");
System.out.println(" - Child span 1: First LLM call");
System.out.println(" - Child span 2: Second LLM call");
System.out.println("\nAll spans ended, will be exported on flush...");
} catch (Exception e) {
rootSpan.recordException(e);
rootSpan.setAttribute("error", true);
throw e;
} finally {
rootSpan.end();
}
}
/**
* Repeats a string a given number of times (Java 8 compatible replacement for String.repeat()).
*
* @param str the string to repeat
* @param count the number of times to repeat
* @return the repeated string
*/
private static String repeatString(String str, int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(str);
}
return sb.toString();
}
/**
* Escapes a JSON string by replacing special characters.
*
* @param str the string to escape
* @return the escaped string
*/
private static String escapeJsonString(String str) {
if (str == null) {
return "";
}
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
@@ -0,0 +1,315 @@
package com.langchain.smith.wrappers.openai.examples;
import com.langchain.smith.wrappers.openai.OpenTelemetryConfig;
import com.langchain.smith.wrappers.openai.WrappedOpenAIClient;
import com.langchain.smith.wrappers.openai.OpenAIWrappers;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionFunctionTool;
import com.openai.models.chat.completions.ChatCompletionTool;
import com.openai.models.chat.completions.ChatCompletionToolChoiceOption;
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
import com.openai.models.FunctionDefinition;
import com.openai.models.FunctionParameters;
import com.openai.models.ChatModel;
import com.openai.core.JsonValue;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import java.util.Arrays;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Example demonstrating how to configure OpenTelemetry to send traces to
* LangSmith.
*
* <p>
* This example shows how to:
* <ul>
* <li>Configure OpenTelemetry to export traces to LangSmith</li>
* <li>Use the wrapped OpenAI client with automatic tracing</li>
* <li>Traces will appear in your LangSmith dashboard</li>
* </ul>
*/
public class WithLangSmithExample {
/**
* Main method demonstrating LangSmith integration.
*
* <p>
* Prerequisites:
* <ol>
* <li>Set OPENAI_API_KEY environment variable</li>
* <li>Set LANGSMITH_API_KEY environment variable (your LangSmith API key)</li>
* <li>Optionally set LANGSMITH_PROJECT environment variable</li>
* <li>Optionally set LANGSMITH_OTLP_ENDPOINT environment variable to override
* the default endpoint</li>
* <li>Optionally set LANGSMITH_DEBUG=true environment variable for detailed
* logging</li>
* </ol>
*
* @param args command line arguments (not used)
*/
public static void main(String[] args) {
// Configure OpenTelemetry for LangSmith with SimpleSpanProcessor for immediate
// export
// This ensures spans are sent immediately without buffering
// To enable debug logging, set LANGSMITH_DEBUG=true environment variable
System.out.println("Configuring OpenTelemetry for LangSmith...");
try {
String apiKey = System.getenv("LANGSMITH_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
throw new IllegalStateException("LANGSMITH_API_KEY environment variable is required");
}
String projectName = System.getenv("LANGSMITH_PROJECT");
String serviceName = System.getenv("OTEL_SERVICE_NAME");
String endpoint = System.getenv("LANGSMITH_OTLP_ENDPOINT");
// Option 1: Use SimpleSpanProcessor for synchronous, immediate export
// This ensures spans are sent immediately without buffering
// SimpleSpanProcessor blocks on span.end() until export completes
// This is the recommended configuration for examples and short-lived
// applications
OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName, endpoint,
OpenTelemetryConfig.SpanProcessorType.SIMPLE, 1);
System.out.println("✓ OpenTelemetry configured with SimpleSpanProcessor (immediate export)\n");
// Option 2: Use BatchSpanProcessor with batch size = 1 for non-blocking
// immediate export
// Uncomment this line and comment the SimpleSpanProcessor configuration above
// to test
// This is non-blocking but still exports immediately due to batch size = 1
// OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName,
// endpoint,
// OpenTelemetryConfig.SpanProcessorType.BATCH, 1);
// System.out.println("✓ OpenTelemetry configured with BatchSpanProcessor (batch
// size = 1)\n");
} catch (IllegalStateException e) {
System.err.println("✗ Error configuring OpenTelemetry: " + e.getMessage());
System.err.println("\nPlease set the following environment variables:");
System.err.println(" - LANGSMITH_API_KEY (required)");
System.err.println(" - LANGSMITH_PROJECT (optional)");
return;
}
// Now use the wrapped client - all API calls will automatically create spans
// that are sent to LangSmith
WrappedOpenAIClient client = OpenAIWrappers.wrapFromEnv();
// Create a chat completion request with tool definitions
// This will trigger tool calls when the model needs to use the get_weather tool
// Build function parameters as JSON schema
Map<String, JsonValue> properties = new HashMap<>();
Map<String, JsonValue> locationProperty = new HashMap<>();
locationProperty.put("type", JsonValue.from("string"));
locationProperty.put("description",
JsonValue.from("The location to get weather for, e.g., 'Paris arrondissement 9'"));
properties.put("location", JsonValue.from(locationProperty));
Map<String, JsonValue> parametersJson = new HashMap<>();
parametersJson.put("type", JsonValue.from("object"));
parametersJson.put("properties", JsonValue.from(properties));
parametersJson.put("required", JsonValue.from(Arrays.asList("location")));
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.addUserMessage("What is the capital of France and what was the temperature there today?")
.tools(Arrays.asList(
ChatCompletionTool.ofFunction(
ChatCompletionFunctionTool.builder()
.function(FunctionDefinition.builder()
.name("get_weather")
.description("Get the current weather for a given location")
.parameters(FunctionParameters.builder()
.putAllAdditionalProperties(parametersJson)
.build())
.build())
.build())))
.toolChoice(ChatCompletionToolChoiceOption.Companion.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO))
.build();
// Create a parent span (chain/workflow) that wraps the entire operation
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
Tracer tracer = openTelemetry.getTracer("langsmith-java-otel-wrappers");
Span parentSpan = tracer.spanBuilder("with_langsmith_example_workflow")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute("gen_ai.operation.name", "chat_completion_workflow")
.setAttribute("langsmith.span.kind", "chain")
.setAttribute("langsmith.trace.name", "with_langsmith_example_workflow")
.startSpan();
try (Scope parentScope = parentSpan.makeCurrent()) {
System.out.println("\n" + repeatString("=", 60));
System.out.println("Workflow: Chat Completion with Tool Call");
System.out.println(repeatString("=", 60));
System.out.println("Parent Span ID: " + parentSpan.getSpanContext().getSpanId());
System.out.println("Parent Trace ID: " + parentSpan.getSpanContext().getTraceId());
// Execute the request - this will automatically create an OpenTelemetry span
// that gets sent to LangSmith (as a child of the parent span)
System.out.println("\nSending chat completion request (traces will be sent to LangSmith)...");
ChatCompletion completion = client.chat().completions().create(params);
// Check if the response contains tool calls
com.openai.models.chat.completions.ChatCompletionMessage message = completion.choices().get(0).message();
java.util.Optional<java.util.List<ChatCompletionMessageToolCall>> toolCallsOpt = message.toolCalls();
if (toolCallsOpt.isPresent() && !toolCallsOpt.get().isEmpty()) {
System.out.println("\n--- Tool calls detected, executing tools ---");
java.util.List<ChatCompletionMessageToolCall> toolCalls = toolCallsOpt.get();
// Build messages list for follow-up request
List<com.openai.models.chat.completions.ChatCompletionMessageParam> messages = new ArrayList<>();
// Add original user message
messages.add(params.messages().get(0));
// Add assistant message with tool calls
messages.add(com.openai.models.chat.completions.ChatCompletionMessageParam.ofAssistant(
com.openai.models.chat.completions.ChatCompletionAssistantMessageParam.builder()
.content(message.content().orElse(""))
.toolCalls(toolCalls)
.build()));
// Execute each tool call and add results
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
if (toolCall.isFunction()) {
ChatCompletionMessageFunctionToolCall functionToolCall = toolCall.asFunction();
String toolName = functionToolCall.function().name();
String toolArguments = functionToolCall.function().arguments();
System.out.println("Executing tool: " + toolName + " with arguments: " + toolArguments);
// Execute the tool
String toolResult = executeTool(toolName, toolArguments);
System.out.println("Tool result: " + toolResult);
// Add tool result message
messages.add(com.openai.models.chat.completions.ChatCompletionMessageParam.ofTool(
com.openai.models.chat.completions.ChatCompletionToolMessageParam.builder()
.toolCallId(functionToolCall.id())
.content(toolResult)
.build()));
}
}
// Send follow-up request with tool results
System.out.println("\nSending follow-up request with tool results...");
ChatCompletionCreateParams followUpParams = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.messages(messages)
.tools(params.tools().orElse(null))
.build();
completion = client.chat().completions().create(followUpParams);
}
// Display the result
String content = completion.choices().get(0).message().content()
.orElse("No content");
System.out.println("\nFinal Response: " + content);
// Usage information
completion.usage().ifPresent(usage -> {
System.out.println("\nToken usage:");
System.out.println(" Input tokens: " + usage.promptTokens());
System.out.println(" Output tokens: " + usage.completionTokens());
System.out.println(" Total tokens: " + usage.totalTokens());
});
// Note: Tool call spans are automatically created by WrappedChatService
// when tool calls are detected in the response. No manual span creation needed!
System.out.println("\n✓ Workflow completed successfully");
System.out.println(" - Parent span: workflow (chain)");
System.out.println(" - Child span: Chat completion (LLM) - automatically created by WrappedChatService");
System.out.println(" - Tool call spans: Automatically created if tool calls are present in the response");
} catch (Exception e) {
parentSpan.recordException(e);
parentSpan.setAttribute("error", true);
System.err.println("✗ Error in workflow: " + e.getMessage());
e.printStackTrace();
} finally {
parentSpan.end();
System.out.println("\n✓ Parent span ended");
}
// Close the client when done
client.close();
// Force flush to ensure all spans are exported
System.out.println("\n" + repeatString("=", 60));
boolean flushed = OpenTelemetryConfig.flush(10, java.util.concurrent.TimeUnit.SECONDS);
if (!flushed) {
System.err.println("✗ Warning: Flush did not complete successfully");
System.err.println(" Some spans may not have been exported to LangSmith");
} else {
System.out.println("✓ Spans flushed successfully");
}
System.out.println("\n✓ Check your LangSmith dashboard to see the traces!");
System.out.println(" Endpoint: " + (System.getenv("LANGSMITH_OTLP_ENDPOINT") != null
? System.getenv("LANGSMITH_OTLP_ENDPOINT")
: OpenTelemetryConfig.LANGSMITH_OTLP_ENDPOINT));
}
/**
* Repeats a string a given number of times (Java 8 compatible replacement for
* String.repeat()).
*
* @param str the string to repeat
* @param count the number of times to repeat
* @return the repeated string
*/
private static String repeatString(String str, int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(str);
}
return sb.toString();
}
/**
* Executes a tool based on its name and arguments.
*
* @param toolName the name of the tool to execute
* @param arguments JSON string containing the tool arguments
* @return JSON string containing the tool result
*/
private static String executeTool(String toolName, String arguments) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode args = mapper.readTree(arguments);
if ("get_weather".equals(toolName)) {
String location = args.has("location") ? args.get("location").asText() : "unknown";
// Simulate weather API call
// In a real implementation, this would call an actual weather API
Map<String, Object> result = new HashMap<>();
result.put("location", location);
result.put("temperature", "15°C");
result.put("condition", "Sunny");
result.put("humidity", "65%");
return mapper.writeValueAsString(result);
} else {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("error", "Unknown tool: " + toolName);
return mapper.writeValueAsString(errorMap);
}
} catch (Exception e) {
return "{\"error\": \"" + e.getMessage() + "\"}";
}
}
}
@@ -0,0 +1,7 @@
/**
* LangSmith OpenTelemetry Wrappers for Java.
*
* <p>
* This package provides OpenTelemetry integration wrappers for LangSmith.
*/
package com.langchain.smith.wrappers;
@@ -0,0 +1,67 @@
package com.langchain.smith.wrappers.openai;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for the WrappedOpenAIClient wrapper.
*/
class WrappedOpenAIClientTest {
@Test
void testWrapWithNullClientThrowsException() {
assertThrows(IllegalArgumentException.class, () -> {
new WrappedOpenAIClient(null);
});
}
@Test
void testWrapDelegatesToOriginalClient() {
// Create a mock or use a real client if API key is available
// For now, we'll test that the wrapper can be created
// In a real scenario, you'd need an API key to test with an actual client
// Test that builder works
WrappedOpenAIClient.Builder builder = WrappedOpenAIClient.builder();
assertNotNull(builder);
// Test that fromEnv() method exists (will fail at runtime if env vars not set, but that's expected)
// WrappedOpenAIClient client = WrappedOpenAIClient.fromEnv();
}
@Test
void testBuilderDelegatesToOpenAIClientBuilder() {
WrappedOpenAIClient.Builder builder = WrappedOpenAIClient.builder();
// Test that builder methods exist and can be chained
builder.apiKey("test-key");
builder.organization("test-org");
builder.project("test-project");
builder.baseUrl("https://api.openai.com/v1");
// Build should succeed (even if API key is invalid)
assertDoesNotThrow(() -> builder.build());
}
@Test
void testOpenAIWrappersWrapMethod() {
// Create a minimal client to wrap
OpenAIClient originalClient = OpenAIOkHttpClient.builder()
.apiKey("test-key")
.build();
WrappedOpenAIClient wrapped = OpenAIWrappers.wrap(originalClient);
assertNotNull(wrapped);
assertEquals(originalClient, wrapped.getDelegate());
}
@Test
void testOpenAIWrappersWrapWithNullThrowsException() {
assertThrows(IllegalArgumentException.class, () -> {
OpenAIWrappers.wrap(null);
});
}
}
@@ -0,0 +1,5 @@
/**
* Test package for LangSmith OpenTelemetry Wrappers.
*/
package com.langchain.smith.wrappers;