diff --git a/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java index aa1332489a6..56bc1f69c88 100644 --- a/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java +++ b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java @@ -5,6 +5,7 @@ import datadog.trace.api.DDTraceId; import datadog.trace.api.WellKnownTags; import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.api.llmobs.LLMObsContext; import datadog.trace.api.llmobs.LLMObsSpan; import datadog.trace.api.llmobs.LLMObsTags; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; @@ -81,8 +82,8 @@ public DDLLMObsSpan( this.span.setTag(LLMOBS_TAG_PREFIX + LLMObsTags.SESSION_ID, sessionId); } - AgentSpanContext parent = LLMObsState.getLLMObsParentContext(); - String parentSpanID = LLMObsState.ROOT_SPAN_ID; + AgentSpanContext parent = LLMObsContext.current(); + String parentSpanID = LLMObsContext.ROOT_SPAN_ID; if (null != parent) { if (parent.getTraceId() != this.span.getTraceId()) { LOGGER.error( @@ -96,8 +97,7 @@ public DDLLMObsSpan( } } this.span.setTag(LLMOBS_TAG_PREFIX + PARENT_ID_TAG_INTERNAL, parentSpanID); - this.scope = LLMObsState.attach(); - LLMObsState.setLLMObsParentContext(this.span.context()); + this.scope = LLMObsContext.attach(this.span.context()); } @Override diff --git a/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/LLMObsState.java b/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/LLMObsState.java deleted file mode 100644 index 84f05afc94a..00000000000 --- a/dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/LLMObsState.java +++ /dev/null @@ -1,37 +0,0 @@ -package datadog.trace.llmobs.domain; - -import datadog.context.Context; -import datadog.context.ContextKey; -import datadog.context.ContextScope; -import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; - -public class LLMObsState { - public static final String ROOT_SPAN_ID = "undefined"; - - private static final ContextKey CONTEXT_KEY = ContextKey.named("llmobs_span"); - - private AgentSpanContext parentSpanID; - - public static ContextScope attach() { - return Context.current().with(CONTEXT_KEY, new LLMObsState()).attach(); - } - - private static LLMObsState fromContext() { - return Context.current().get(CONTEXT_KEY); - } - - public static AgentSpanContext getLLMObsParentContext() { - LLMObsState state = fromContext(); - if (state != null) { - return state.parentSpanID; - } - return null; - } - - public static void setLLMObsParentContext(AgentSpanContext llmObsParentContext) { - LLMObsState state = fromContext(); - if (state != null) { - state.parentSpanID = llmObsParentContext; - } - } -} diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java index 62ddf7c32fe..fe905a56383 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java @@ -108,7 +108,12 @@ public static ReferenceMatcher loadStaticMuzzleReferences( } /** - * @return Class names of helpers to inject into the user's classloader + * @return Class names of helpers to inject into the user's classloader. + *
+ *

NOTE: The order matters. If the muzzle check fails with a NoClassDefFoundError + * (as seen in build/reports/muzzle-*.txt), it may be because some helper classes depend on + * each other. In this case, the order must be adjusted accordingly. + *

*/ public String[] helperClassNames() { return NO_HELPERS; diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/build.gradle b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/build.gradle new file mode 100644 index 00000000000..5be648e0e14 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/build.gradle @@ -0,0 +1,25 @@ +apply from: "$rootDir/gradle/java.gradle" +apply plugin: 'idea' + +def minVer = '3.0.0' + +muzzle { + pass { + group = "com.openai" + module = "openai-java" + versions = "[$minVer,)" + } +} + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + compileOnly group: 'com.openai', name: 'openai-java', version: minVer + implementation project(':internal-api') + + testImplementation group: 'com.openai', name: 'openai-java', version: minVer + latestDepTestImplementation group: 'com.openai', name: 'openai-java', version: '+' + + testImplementation project(':dd-java-agent:instrumentation:okhttp:okhttp-3.0') +} + diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/gradle.lockfile b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/gradle.lockfile new file mode 100644 index 00000000000..67886968096 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/gradle.lockfile @@ -0,0 +1,156 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +cafe.cryptography:curve25519-elisabeth:0.1.0=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +cafe.cryptography:ed25519-elisabeth:0.1.0=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +ch.qos.logback:logback-classic:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.blogspot.mydailyjava:weak-lock-free:0.17=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okhttp3:okhttp:3.12.15=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okio:okio:1.17.6=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-instrument-java:0.0.3=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-javac-plugin-client:0.2.2=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:java-dogstatsd-client:4.4.3=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.datadoghq:sketches-java:0.8.3=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.18.2=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.18.2=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.18.2=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml:classmate:1.7.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.javaparser:javaparser-core:3.25.6=codenarc +com.github.jnr:jffi:1.3.13=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-a64asm:1.0.0=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-constants:0.10.4=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-enxio:0.32.17=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-ffi:2.2.16=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-posix:3.1.19=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-unixsocket:0.38.22=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-x86asm:1.0.2=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs:4.9.8=spotbugs +com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs +com.github.victools:jsonschema-generator:4.38.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.victools:jsonschema-module-jackson:4.38.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.victools:jsonschema-module-swagger-2:4.38.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,testAnnotationProcessor,testCompileClasspath +com.google.auto.service:auto-service:1.1.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.auto:auto-common:1.2.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.13.2=spotbugs +com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.errorprone:error_prone_annotations:2.33.0=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.41.0=spotbugs +com.google.guava:failureaccess:1.0.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.guava:guava:20.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.0.1-jre=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +com.google.re2j:re2j:1.7=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.openai:openai-java-client-okhttp:3.0.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +com.openai:openai-java-client-okhttp:4.13.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath +com.openai:openai-java-core:3.0.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +com.openai:openai-java-core:4.13.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath +com.openai:openai-java:3.0.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +com.openai:openai-java:4.13.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath +com.squareup.moshi:moshi:1.11.0=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:logging-interceptor:3.12.12=latestDepTestCompileClasspath,testCompileClasspath +com.squareup.okhttp3:logging-interceptor:4.12.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:3.12.12=latestDepTestCompileClasspath,testCompileClasspath +com.squareup.okhttp3:okhttp:4.12.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.squareup.okio:okio:1.17.5=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath +com.squareup.okio:okio:3.6.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +com.thoughtworks.qdox:qdox:1.12.1=codenarc +commons-fileupload:commons-fileupload:1.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.20.0=spotbugs +de.thetaphi:forbiddenapis:3.10=compileClasspath +io.leangen.geantyref:geantyref:1.3.16=latestDepTestRuntimeClasspath,testRuntimeClasspath +io.sqreen:libsqreen:17.3.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.31=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:3.1.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +jaxen:jaxen:2.0.0=spotbugs +junit:junit:4.13.2=latestDepTestRuntimeClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.1=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.1=compileClasspath,instrumentPluginClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.8.0=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.8.0=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +net.sf.saxon:Saxon-HE:12.9=spotbugs +org.apache.ant:ant-antlr:1.10.14=codenarc +org.apache.ant:ant-junit:1.10.14=codenarc +org.apache.bcel:bcel:6.11.0=spotbugs +org.apache.commons:commons-lang3:3.19.0=spotbugs +org.apache.commons:commons-text:1.14.0=spotbugs +org.apache.httpcomponents.client5:httpclient5:5.3.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.apache.httpcomponents.core5:httpcore5-h2:5.2.4=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.apache.httpcomponents.core5:httpcore5:5.2.4=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-api:2.25.2=spotbugs +org.apache.logging.log4j:log4j-core:2.25.2=spotbugs +org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,testCompileClasspath +org.checkerframework:checker-qual:3.33.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor +org.codehaus.groovy:groovy-ant:3.0.23=codenarc +org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc +org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.groovy:groovy-templates:3.0.23=codenarc +org.codehaus.groovy:groovy-xml:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codenarc:CodeNarc:3.7.0=codenarc +org.dom4j:dom4j:2.2.0=spotbugs +org.gmetrics:GMetrics:2.1.0=codenarc +org.hamcrest:hamcrest-core:1.3=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jctools:jctools-core:3.3.0=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-reflect:1.8.10=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0=compileClasspath,latestDepTestCompileClasspath,testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=compileClasspath,latestDepTestCompileClasspath,testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=compileClasspath,latestDepTestCompileClasspath,testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.8.0=compileClasspath,latestDepTestCompileClasspath,testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.9.10=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-runner:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-suite-api:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-suite-commons:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit:junit-bom:5.14.0=spotbugs +org.junit:junit-bom:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:4.4.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.2=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.9=spotbugs +org.ow2.asm:asm-commons:9.2=instrumentPluginClasspath,muzzleTooling,runtimeClasspath +org.ow2.asm:asm-commons:9.9=latestDepTestRuntimeClasspath,spotbugs,testRuntimeClasspath +org.ow2.asm:asm-tree:9.2=instrumentPluginClasspath,muzzleTooling,runtimeClasspath +org.ow2.asm:asm-tree:9.9=latestDepTestRuntimeClasspath,spotbugs,testRuntimeClasspath +org.ow2.asm:asm-util:9.2=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-util:9.9=spotbugs +org.ow2.asm:asm:9.2=instrumentPluginClasspath,muzzleTooling,runtimeClasspath +org.ow2.asm:asm:9.9=latestDepTestRuntimeClasspath,spotbugs,testRuntimeClasspath +org.slf4j:jcl-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:log4j-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.30=compileClasspath,instrumentPluginClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath +org.slf4j:slf4j-api:1.7.32=latestDepTestCompileClasspath,testCompileClasspath +org.slf4j:slf4j-api:2.0.16=latestDepTestRuntimeClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j +org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j +org.snakeyaml:snakeyaml-engine:2.9=instrumentPluginClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.spockframework:spock-bom:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.spockframework:spock-core:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.xmlresolver:xmlresolver:5.3.3=spotbugs +empty=spotbugsPlugins diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionDecorator.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionDecorator.java new file mode 100644 index 00000000000..37452d2b5bf --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionDecorator.java @@ -0,0 +1,157 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.REQUEST_MODEL; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.RESPONSE_MODEL; + +import com.openai.helpers.ChatCompletionAccumulator; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionChunk; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import com.openai.models.chat.completions.ChatCompletionMessage; +import com.openai.models.chat.completions.ChatCompletionMessageParam; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import datadog.trace.api.Config; +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ChatCompletionDecorator { + public static final ChatCompletionDecorator DECORATE = new ChatCompletionDecorator(); + private static final CharSequence CHAT_COMPLETIONS_CREATE = + UTF8BytesString.create("createChatCompletion"); + + private final boolean llmObsEnabled = Config.get().isLlmObsEnabled(); + + public void withChatCompletionCreateParams( + AgentSpan span, ChatCompletionCreateParams params, boolean stream) { + span.setResourceName(CHAT_COMPLETIONS_CREATE); + span.setTag("openai.request.endpoint", "v1/chat/completions"); + span.setTag("openai.request.method", "POST"); + if (!llmObsEnabled) { + return; + } + + span.setTag("_ml_obs_tag.span.kind", Tags.LLMOBS_LLM_SPAN_KIND); + if (params == null) { + return; + } + params.model()._value().asString().ifPresent(str -> span.setTag(REQUEST_MODEL, str)); + + span.setTag( + "_ml_obs_tag.input", + params.messages().stream() + .map(ChatCompletionDecorator::llmMessage) + .collect(Collectors.toList())); + + Map metadata = new HashMap<>(); + // maxTokens is deprecated but integration tests missing to provide maxCompletionTokens + params.maxTokens().ifPresent(v -> metadata.put("max_tokens", v)); + params.temperature().ifPresent(v -> metadata.put("temperature", v)); + if (stream) { + metadata.put("stream", true); + } + params + .streamOptions() + .ifPresent( + v -> { + if (v.includeUsage().orElse(false)) { + metadata.put("stream_options", Collections.singletonMap("include_usage", true)); + } + }); + span.setTag("_ml_obs_tag.metadata", metadata); + } + + private static LLMObs.LLMMessage llmMessage(ChatCompletionMessageParam m) { + String role = "unknown"; + String content = null; + if (m.isAssistant()) { + role = "assistant"; + content = m.asAssistant().content().map(v -> v.text().orElse(null)).orElse(null); + } else if (m.isDeveloper()) { + role = "developer"; + content = m.asDeveloper().content().text().orElse(null); + } else if (m.isSystem()) { + role = "system"; + content = m.asSystem().content().text().orElse(null); + } else if (m.isTool()) { + role = "tool"; + content = m.asTool().content().text().orElse(null); + } else if (m.isUser()) { + role = "user"; + content = m.asUser().content().text().orElse(null); + } + return LLMObs.LLMMessage.from(role, content); + } + + public void withChatCompletion(AgentSpan span, ChatCompletion completion) { + if (!llmObsEnabled) { + return; + } + String modelName = completion.model(); + span.setTag(RESPONSE_MODEL, modelName); + span.setTag("_ml_obs_tag.model_name", modelName); + span.setTag("_ml_obs_tag.model_provider", "openai"); + + List output = + completion.choices().stream() + .map(ChatCompletionDecorator::llmMessage) + .collect(Collectors.toList()); + span.setTag("_ml_obs_tag.output", output); + + completion + .usage() + .ifPresent( + usage -> { + span.setTag("_ml_obs_metric.input_tokens", usage.promptTokens()); + span.setTag("_ml_obs_metric.output_tokens", usage.completionTokens()); + span.setTag("_ml_obs_metric.total_tokens", usage.totalTokens()); + }); + } + + private static LLMObs.LLMMessage llmMessage(ChatCompletion.Choice choice) { + ChatCompletionMessage msg = choice.message(); + Optional roleOpt = msg._role().asString(); + String role = "unknown"; + if (roleOpt.isPresent()) { + role = String.valueOf(roleOpt.get()); + } + String content = msg.content().orElse(null); + + Optional> toolCallsOpt = msg.toolCalls(); + if (toolCallsOpt.isPresent() && !toolCallsOpt.get().isEmpty()) { + List toolCalls = new ArrayList<>(); + for (ChatCompletionMessageToolCall toolCall : toolCallsOpt.get()) { + LLMObs.ToolCall llmObsToolCall = ToolCallExtractor.getToolCall(toolCall); + if (llmObsToolCall != null) { + toolCalls.add(llmObsToolCall); + } + } + + if (!toolCalls.isEmpty()) { + return LLMObs.LLMMessage.from(role, content, toolCalls); + } + } + + return LLMObs.LLMMessage.from(role, content); + } + + public void withChatCompletionChunks(AgentSpan span, List chunks) { + if (!llmObsEnabled) { + return; + } + ChatCompletionAccumulator accumulator = ChatCompletionAccumulator.create(); + for (ChatCompletionChunk chunk : chunks) { + accumulator.accumulate(chunk); + } + ChatCompletion chatCompletion = accumulator.chatCompletion(); + withChatCompletion(span, chatCompletion); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionModule.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionModule.java new file mode 100644 index 00000000000..7d36b2612f3 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionModule.java @@ -0,0 +1,34 @@ +package datadog.trace.instrumentation.openai_java; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumenterModule.class) +public class ChatCompletionModule extends InstrumenterModule.Tracing { + public ChatCompletionModule() { + super("openai-java"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".ChatCompletionDecorator", + packageName + ".OpenAiDecorator", + packageName + ".HttpResponseWrapper", + packageName + ".HttpStreamResponseWrapper", + packageName + ".HttpStreamResponseStreamWrapper", + packageName + ".ToolCallExtractor", + packageName + ".ToolCallExtractor$1" + }; + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new ChatCompletionServiceAsyncInstrumentation(), + new ChatCompletionServiceInstrumentation()); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionServiceAsyncInstrumentation.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionServiceAsyncInstrumentation.java new file mode 100644 index 00000000000..608c2088d96 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionServiceAsyncInstrumentation.java @@ -0,0 +1,104 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.openai.core.ClientOptions; +import com.openai.core.http.HttpResponseFor; +import com.openai.core.http.StreamResponse; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionChunk; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.asm.Advice; + +public class ChatCompletionServiceAsyncInstrumentation + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + @Override + public String instrumentedType() { + return "com.openai.services.async.chat.ChatCompletionServiceAsyncImpl$WithRawResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("create")) + .and( + takesArgument( + 0, named("com.openai.models.chat.completions.ChatCompletionCreateParams"))) + .and(returns(named(CompletableFuture.class.getName()))), + getClass().getName() + "$CreateAdvice"); + + transformer.applyAdvice( + isMethod() + .and(named("createStreaming")) + .and( + takesArgument( + 0, named("com.openai.models.chat.completions.ChatCompletionCreateParams"))) + .and(returns(named(CompletableFuture.class.getName()))), + getClass().getName() + "$CreateStreamingAdvice"); + } + + public static class CreateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ChatCompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ChatCompletionDecorator.DECORATE.withChatCompletionCreateParams(span, params, false); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) CompletableFuture> future, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || future == null) { + DECORATE.finishSpan(span, err); + } else { + future = + HttpResponseWrapper.wrapFuture( + future, span, ChatCompletionDecorator.DECORATE::withChatCompletion); + } + scope.close(); + } + } + + public static class CreateStreamingAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ChatCompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ChatCompletionDecorator.DECORATE.withChatCompletionCreateParams(span, params, true); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) + CompletableFuture>> future, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || future == null) { + DECORATE.finishSpan(span, err); + } else { + future = + HttpStreamResponseWrapper.wrapFuture( + future, span, ChatCompletionDecorator.DECORATE::withChatCompletionChunks); + } + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionServiceInstrumentation.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionServiceInstrumentation.java new file mode 100644 index 00000000000..4dcaa81ca4c --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ChatCompletionServiceInstrumentation.java @@ -0,0 +1,104 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.openai.core.ClientOptions; +import com.openai.core.http.HttpResponseFor; +import com.openai.core.http.StreamResponse; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionChunk; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import net.bytebuddy.asm.Advice; + +public class ChatCompletionServiceInstrumentation + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + @Override + public String instrumentedType() { + return "com.openai.services.blocking.chat.ChatCompletionServiceImpl$WithRawResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("create")) + .and( + takesArgument( + 0, named("com.openai.models.chat.completions.ChatCompletionCreateParams"))) + .and(returns(named("com.openai.core.http.HttpResponseFor"))), + getClass().getName() + "$CreateAdvice"); + + transformer.applyAdvice( + isMethod() + .and(named("createStreaming")) + .and( + takesArgument( + 0, named("com.openai.models.chat.completions.ChatCompletionCreateParams"))) + .and(returns(named("com.openai.core.http.HttpResponseFor"))), + getClass().getName() + "$CreateStreamingAdvice"); + } + + public static class CreateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ChatCompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ChatCompletionDecorator.DECORATE.withChatCompletionCreateParams(span, params, false); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) HttpResponseFor response, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || response == null) { + DECORATE.finishSpan(span, err); + } else { + response = + HttpResponseWrapper.wrap( + response, span, ChatCompletionDecorator.DECORATE::withChatCompletion); + } + scope.close(); + } + } + + public static class CreateStreamingAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ChatCompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ChatCompletionDecorator.DECORATE.withChatCompletionCreateParams(span, params, true); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) + HttpResponseFor> response, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || response == null) { + DECORATE.finishSpan(span, err); + } else { + response = + HttpStreamResponseWrapper.wrap( + response, span, ChatCompletionDecorator.DECORATE::withChatCompletionChunks); + } + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionDecorator.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionDecorator.java new file mode 100644 index 00000000000..994a4104565 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionDecorator.java @@ -0,0 +1,90 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.REQUEST_MODEL; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.RESPONSE_MODEL; + +import com.openai.models.completions.Completion; +import com.openai.models.completions.CompletionCreateParams; +import datadog.trace.api.Config; +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CompletionDecorator { + public static final CompletionDecorator DECORATE = new CompletionDecorator(); + + private static final CharSequence COMPLETIONS_CREATE = UTF8BytesString.create("createCompletion"); + + private final boolean llmObsEnabled = Config.get().isLlmObsEnabled(); + + public void withCompletionCreateParams(AgentSpan span, CompletionCreateParams params) { + span.setResourceName(COMPLETIONS_CREATE); + span.setTag("openai.request.endpoint", "v1/completions"); + span.setTag("openai.request.method", "POST"); + if (!llmObsEnabled) { + return; + } + + span.setTag("_ml_obs_tag.span.kind", Tags.LLMOBS_LLM_SPAN_KIND); + if (params == null) { + return; + } + + params.model()._value().asString().ifPresent(str -> span.setTag(REQUEST_MODEL, str)); + params + .prompt() + .flatMap(p -> p.string()) + .ifPresent( + input -> + span.setTag( + "_ml_obs_tag.input", + Collections.singletonList(LLMObs.LLMMessage.from(null, input)))); + + Map metadata = new HashMap<>(); + params.maxTokens().ifPresent(v -> metadata.put("max_tokens", v)); + params.temperature().ifPresent(v -> metadata.put("temperature", v)); + span.setTag("_ml_obs_tag.metadata", metadata); + } + + public void withCompletion(AgentSpan span, Completion completion) { + if (!llmObsEnabled) { + return; + } + + String modelName = completion.model(); + span.setTag(RESPONSE_MODEL, modelName); + span.setTag("_ml_obs_tag.model_name", modelName); + span.setTag("_ml_obs_tag.model_provider", "openai"); + + List output = + completion.choices().stream() + .map(v -> LLMObs.LLMMessage.from(null, v.text())) + .collect(Collectors.toList()); + span.setTag("_ml_obs_tag.output", output); + + completion + .usage() + .ifPresent( + usage -> { + span.setTag("_ml_obs_metric.input_tokens", usage.promptTokens()); + span.setTag("_ml_obs_metric.output_tokens", usage.completionTokens()); + span.setTag("_ml_obs_metric.total_tokens", usage.totalTokens()); + }); + } + + public void withCompletions(AgentSpan span, List completions) { + if (!llmObsEnabled) { + return; + } + + if (!completions.isEmpty()) { + withCompletion(span, completions.get(0)); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionModule.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionModule.java new file mode 100644 index 00000000000..9abfc2f83f2 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionModule.java @@ -0,0 +1,31 @@ +package datadog.trace.instrumentation.openai_java; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumenterModule.class) +public class CompletionModule extends InstrumenterModule.Tracing { + public CompletionModule() { + super("openai-java"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".CompletionDecorator", + packageName + ".OpenAiDecorator", + packageName + ".HttpResponseWrapper", + packageName + ".HttpStreamResponseWrapper", + packageName + ".HttpStreamResponseStreamWrapper", + }; + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new CompletionServiceAsyncInstrumentation(), new CompletionServiceInstrumentation()); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionServiceAsyncInstrumentation.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionServiceAsyncInstrumentation.java new file mode 100644 index 00000000000..745a6ff4bdd --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionServiceAsyncInstrumentation.java @@ -0,0 +1,100 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.openai.core.ClientOptions; +import com.openai.core.http.HttpResponseFor; +import com.openai.core.http.StreamResponse; +import com.openai.models.completions.Completion; +import com.openai.models.completions.CompletionCreateParams; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.asm.Advice; + +public class CompletionServiceAsyncInstrumentation + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + @Override + public String instrumentedType() { + return "com.openai.services.async.CompletionServiceAsyncImpl$WithRawResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("create")) + .and(takesArgument(0, named("com.openai.models.completions.CompletionCreateParams"))) + .and(returns(named(CompletableFuture.class.getName()))), + getClass().getName() + "$CreateAdvice"); + + transformer.applyAdvice( + isMethod() + .and(named("createStreaming")) + .and(takesArgument(0, named("com.openai.models.completions.CompletionCreateParams"))) + .and(returns(named(CompletableFuture.class.getName()))), + getClass().getName() + "$CreateStreamingAdvice"); + } + + public static class CreateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final CompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + CompletionDecorator.DECORATE.withCompletionCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) CompletableFuture> future, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || future == null) { + DECORATE.finishSpan(span, err); + } else { + future = + HttpResponseWrapper.wrapFuture( + future, span, CompletionDecorator.DECORATE::withCompletion); + } + scope.close(); + } + } + + public static class CreateStreamingAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final CompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + CompletionDecorator.DECORATE.withCompletionCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) + CompletableFuture>> future, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || future == null) { + DECORATE.finishSpan(span, err); + } else { + future = + HttpStreamResponseWrapper.wrapFuture( + future, span, CompletionDecorator.DECORATE::withCompletions); + } + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionServiceInstrumentation.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionServiceInstrumentation.java new file mode 100644 index 00000000000..6dadd4fecd4 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/CompletionServiceInstrumentation.java @@ -0,0 +1,104 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.openai.core.ClientOptions; +import com.openai.core.http.HttpResponseFor; +import com.openai.core.http.StreamResponse; +import com.openai.models.completions.Completion; +import com.openai.models.completions.CompletionCreateParams; +import datadog.context.ContextScope; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import net.bytebuddy.asm.Advice; + +public class CompletionServiceInstrumentation + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + @Override + public String instrumentedType() { + return "com.openai.services.blocking.CompletionServiceImpl$WithRawResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("create")) + .and(takesArgument(0, named("com.openai.models.completions.CompletionCreateParams"))) + .and(returns(named("com.openai.core.http.HttpResponseFor"))), + getClass().getName() + "$CreateAdvice"); + + transformer.applyAdvice( + isMethod() + .and(named("createStreaming")) + .and(takesArgument(0, named("com.openai.models.completions.CompletionCreateParams"))) + .and(returns(named("com.openai.core.http.HttpResponseFor"))), + getClass().getName() + "$CreateStreamingAdvice"); + } + + public static class CreateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final CompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions, + @Advice.Local("llmScope") ContextScope llmScope) { + AgentSpan span = DECORATE.startSpan(clientOptions); + // llmScope = LLMObsContext.attach(span.context()); + // TODO why would we ever need to activate llmScope in this instrumentation if we never expect + // inner llmobs spans + CompletionDecorator.DECORATE.withCompletionCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Local("llmScope") ContextScope llmScope, + @Advice.Return(readOnly = false) HttpResponseFor response, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || response == null) { + DECORATE.finishSpan(span, err); + } else { + response = + HttpResponseWrapper.wrap(response, span, CompletionDecorator.DECORATE::withCompletion); + } + scope.close(); + // llmScope.close(); + } + } + + public static class CreateStreamingAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final CompletionCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + CompletionDecorator.DECORATE.withCompletionCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) HttpResponseFor> response, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || response == null) { + DECORATE.finishSpan(span, err); + } else { + response = + HttpStreamResponseWrapper.wrap( + response, span, CompletionDecorator.DECORATE::withCompletions); + } + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingDecorator.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingDecorator.java new file mode 100644 index 00000000000..750ceaf64eb --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingDecorator.java @@ -0,0 +1,80 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.REQUEST_MODEL; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.RESPONSE_MODEL; + +import com.openai.models.embeddings.CreateEmbeddingResponse; +import com.openai.models.embeddings.Embedding; +import com.openai.models.embeddings.EmbeddingCreateParams; +import datadog.trace.api.Config; +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class EmbeddingDecorator { + public static final EmbeddingDecorator DECORATE = new EmbeddingDecorator(); + + private static final CharSequence EMBEDDINGS_CREATE = UTF8BytesString.create("createEmbedding"); + + private final boolean llmObsEnabled = Config.get().isLlmObsEnabled(); + + public void withEmbeddingCreateParams(AgentSpan span, EmbeddingCreateParams params) { + span.setResourceName(EMBEDDINGS_CREATE); + span.setTag("openai.request.endpoint", "v1/embeddings"); + span.setTag("openai.request.method", "POST"); + if (!llmObsEnabled) { + return; + } + + span.setTag("_ml_obs_tag.span.kind", Tags.LLMOBS_EMBEDDING_SPAN_KIND); + if (params == null) { + return; + } + params.model()._value().asString().ifPresent(str -> span.setTag(REQUEST_MODEL, str)); + + span.setTag("_ml_obs_tag.input", embeddingDocuments(params.input())); + + Map metadata = new HashMap<>(); + Optional encodingFormat = params.encodingFormat().flatMap(v -> v._value().asString()); + encodingFormat.ifPresent(v -> metadata.put("encoding_format", v)); + params.dimensions().ifPresent(v -> metadata.put("dimensions", v)); + span.setTag("_ml_obs_tag.metadata", metadata); + } + + private List embeddingDocuments(EmbeddingCreateParams.Input input) { + List inputs = Collections.emptyList(); + if (input.isString()) { + inputs = Collections.singletonList(input.asString()); + } else if (input.isArrayOfStrings()) { + inputs = input.asArrayOfStrings(); + } + return inputs.stream().map(LLMObs.Document::from).collect(Collectors.toList()); + } + + public void withCreateEmbeddingResponse(AgentSpan span, CreateEmbeddingResponse response) { + if (!llmObsEnabled) { + return; + } + + String modelName = response.model(); + span.setTag(RESPONSE_MODEL, modelName); + span.setTag("_ml_obs_tag.model_name", modelName); + span.setTag("_ml_obs_tag.model_provider", "openai"); + + if (!response.data().isEmpty()) { + int embeddingCount = response.data().size(); + Embedding firstEmbedding = response.data().get(0); + int embeddingSize = firstEmbedding.embedding().size(); + span.setTag( + "_ml_obs_tag.output", + String.format("[%d embedding(s) returned with size %d]", embeddingCount, embeddingSize)); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingModule.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingModule.java new file mode 100644 index 00000000000..925d4be8f8a --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingModule.java @@ -0,0 +1,30 @@ +package datadog.trace.instrumentation.openai_java; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumenterModule.class) +public class EmbeddingModule extends InstrumenterModule.Tracing { + public EmbeddingModule() { + super("openai-java"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".EmbeddingDecorator", + packageName + ".OpenAiDecorator", + packageName + ".HttpResponseWrapper", + packageName + ".HttpStreamResponseWrapper", + packageName + ".HttpStreamResponseStreamWrapper", + }; + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new EmbeddingServiceInstrumentation()); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingServiceInstrumentation.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingServiceInstrumentation.java new file mode 100644 index 00000000000..cac8d7abddf --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/EmbeddingServiceInstrumentation.java @@ -0,0 +1,62 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.openai.core.ClientOptions; +import com.openai.core.http.HttpResponseFor; +import com.openai.models.embeddings.CreateEmbeddingResponse; +import com.openai.models.embeddings.EmbeddingCreateParams; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import net.bytebuddy.asm.Advice; + +public class EmbeddingServiceInstrumentation + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + @Override + public String instrumentedType() { + return "com.openai.services.blocking.EmbeddingServiceImpl$WithRawResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("create")) + .and(takesArgument(0, named("com.openai.models.embeddings.EmbeddingCreateParams"))) + .and(returns(named("com.openai.core.http.HttpResponseFor"))), + getClass().getName() + "$CreateAdvice"); + } + + public static class CreateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final EmbeddingCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + EmbeddingDecorator.DECORATE.withEmbeddingCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) HttpResponseFor response, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || response == null) { + DECORATE.finishSpan(span, err); + } else { + response = + HttpResponseWrapper.wrap( + response, span, EmbeddingDecorator.DECORATE::withCreateEmbeddingResponse); + } + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/FunctionCallOutputExtractor.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/FunctionCallOutputExtractor.java new file mode 100644 index 00000000000..84015f0a0a0 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/FunctionCallOutputExtractor.java @@ -0,0 +1,80 @@ +package datadog.trace.instrumentation.openai_java; + +import com.openai.models.responses.ResponseInputItem; +import datadog.trace.util.MethodHandles; +import java.lang.invoke.MethodHandle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class to handle FunctionCallOutput.output() method changes between openai-java versions. + * + *

In version 3.x: output() returns String In version 4.0+: output() returns Output) + */ +public class FunctionCallOutputExtractor { + private static final Logger log = LoggerFactory.getLogger(FunctionCallOutputExtractor.class); + + private static final MethodHandles METHOD_HANDLES = + new MethodHandles(ResponseInputItem.FunctionCallOutput.class.getClassLoader()); + + private static final MethodHandle OUTPUT_METHOD; + private static final MethodHandle IS_STRING_METHOD; + private static final MethodHandle AS_STRING_METHOD; + + static { + OUTPUT_METHOD = METHOD_HANDLES.method(ResponseInputItem.FunctionCallOutput.class, "output"); + + Class outputClass = null; + try { + outputClass = + ResponseInputItem.FunctionCallOutput.class + .getClassLoader() + .loadClass("com.openai.models.responses.ResponseInputItem$FunctionCallOutput$Output"); + } catch (ClassNotFoundException e) { + // Output class not found, assuming openai-java version 3.x + } + + if (outputClass != null) { + IS_STRING_METHOD = METHOD_HANDLES.method(outputClass, "isString"); + AS_STRING_METHOD = METHOD_HANDLES.method(outputClass, "asString"); + } else { + IS_STRING_METHOD = null; + AS_STRING_METHOD = null; + } + } + + public static String getOutputAsString(ResponseInputItem.FunctionCallOutput functionCallOutput) { + try { + Object output = METHOD_HANDLES.invoke(OUTPUT_METHOD, functionCallOutput); + + if (output == null) { + return null; + } + + // In v3.x, output() returns String directly + if (output instanceof String) { + return (String) output; + } + + // In v4.0+, output() returns an Output object + if (IS_STRING_METHOD != null && AS_STRING_METHOD != null) { + Boolean isString = METHOD_HANDLES.invoke(IS_STRING_METHOD, output); + if (Boolean.TRUE.equals(isString)) { + return METHOD_HANDLES.invoke(AS_STRING_METHOD, output); + } else { + log.debug("FunctionCallOutput.output() returned non-string Output type, skipping"); + return null; + } + } + + log.debug( + "Unable to extract string from FunctionCallOutput.output(): unexpected return type {}", + output.getClass().getName()); + return null; + + } catch (Exception e) { + log.debug("Error extracting output from FunctionCallOutput", e); + return null; + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpResponseWrapper.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpResponseWrapper.java new file mode 100644 index 00000000000..b78234476db --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpResponseWrapper.java @@ -0,0 +1,91 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; + +import com.openai.core.http.Headers; +import com.openai.core.http.HttpResponseFor; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.io.InputStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class HttpResponseWrapper implements HttpResponseFor { + private static final Logger log = LoggerFactory.getLogger(HttpResponseWrapper.class); + + public static HttpResponseFor wrap( + HttpResponseFor response, AgentSpan span, BiConsumer decorate) { + DECORATE.withHttpResponse(span, response.headers()); + return new HttpResponseWrapper<>(response, span, decorate); + } + + public static CompletableFuture> wrapFuture( + CompletableFuture> future, + AgentSpan span, + BiConsumer decorate) { + return future + .thenApply(response -> wrap(response, span, decorate)) + .whenComplete((_r, t) -> DECORATE.finishSpan(span, t)); + } + + private final HttpResponseFor delegate; + private final AgentSpan span; + private final BiConsumer decorate; + private final AtomicBoolean finished = new AtomicBoolean(false); + + private HttpResponseWrapper( + HttpResponseFor delegate, AgentSpan span, BiConsumer decorate) { + this.delegate = delegate; + this.span = span; + this.decorate = decorate; + } + + @Override + public T parse() { + T parsed; + try { + parsed = delegate.parse(); + } catch (Throwable err) { + DECORATE.finishSpan(span, err); + finished.set(true); + throw err; + } + try { + decorate.accept(span, parsed); + } catch (Throwable t) { + log.debug("Span decorator failed", t); + } finally { + DECORATE.finishSpan(span, null); + finished.set(true); + } + return parsed; + } + + @Override + public int statusCode() { + return delegate.statusCode(); + } + + @NotNull + @Override + public Headers headers() { + return delegate.headers(); + } + + @NotNull + @Override + public InputStream body() { + return delegate.body(); + } + + @Override + public void close() { + if (finished.compareAndSet(false, true)) { + DECORATE.finishSpan(span, null); + } + delegate.close(); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpStreamResponseStreamWrapper.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpStreamResponseStreamWrapper.java new file mode 100644 index 00000000000..06b4ee6b232 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpStreamResponseStreamWrapper.java @@ -0,0 +1,51 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; + +import com.openai.core.http.StreamResponse; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class HttpStreamResponseStreamWrapper implements StreamResponse { + private static final Logger log = LoggerFactory.getLogger(HttpStreamResponseStreamWrapper.class); + + private final AgentSpan span; + private final BiConsumer> decorate; + private final List chunks; + private final StreamResponse parsed; + private final AtomicBoolean finished = new AtomicBoolean(false); + + HttpStreamResponseStreamWrapper( + AgentSpan span, BiConsumer> decorate, StreamResponse parsed) { + this.span = span; + this.decorate = decorate; + this.parsed = parsed; + chunks = new ArrayList<>(); + } + + @NotNull + @Override + public Stream stream() { + return parsed.stream().peek(chunks::add).onClose(this::close); + } + + @Override + public void close() { + if (finished.compareAndSet(false, true)) { + try { + decorate.accept(span, chunks); + } catch (Throwable t) { + log.debug("Span decorator failed", t); + } + DECORATE.finishSpan(span, null); + } + parsed.close(); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpStreamResponseWrapper.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpStreamResponseWrapper.java new file mode 100644 index 00000000000..95f2d0882cf --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/HttpStreamResponseWrapper.java @@ -0,0 +1,91 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; + +import com.openai.core.http.Headers; +import com.openai.core.http.HttpResponseFor; +import com.openai.core.http.StreamResponse; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import org.jetbrains.annotations.NotNull; + +public final class HttpStreamResponseWrapper implements HttpResponseFor> { + + public static HttpResponseFor> wrap( + HttpResponseFor> response, + final AgentSpan span, + BiConsumer> decorate) { + DECORATE.withHttpResponse(span, response.headers()); + return new HttpStreamResponseWrapper<>(response, span, decorate); + } + + public static CompletableFuture>> wrapFuture( + CompletableFuture>> future, + AgentSpan span, + BiConsumer> decorate) { + return future + .thenApply(r -> wrap(r, span, decorate)) + .whenComplete( + (_r, err) -> { + if (err != null) { + DECORATE.finishSpan(span, err); + } + }); + } + + private final HttpResponseFor> delegate; + private final AgentSpan span; + private final BiConsumer> decorate; + private final AtomicBoolean parseCalled = new AtomicBoolean(false); + + private HttpStreamResponseWrapper( + HttpResponseFor> delegate, + AgentSpan span, + BiConsumer> decorate) { + this.delegate = delegate; + this.span = span; + this.decorate = decorate; + } + + @Override + public StreamResponse parse() { + try { + StreamResponse parsed = delegate.parse(); + return new HttpStreamResponseStreamWrapper<>(span, decorate, parsed); + } catch (Throwable err) { + DECORATE.finishSpan(span, err); + throw err; + } finally { + parseCalled.set(true); + } + } + + @Override + public int statusCode() { + return delegate.statusCode(); + } + + @NotNull + @Override + public Headers headers() { + return delegate.headers(); + } + + @NotNull + @Override + public InputStream body() { + return delegate.body(); + } + + @Override + public void close() { + if (parseCalled.compareAndSet(false, true)) { + DECORATE.finishSpan(span, null); + } + delegate.close(); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/OpenAiDecorator.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/OpenAiDecorator.java new file mode 100644 index 00000000000..43d9bcb5aea --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/OpenAiDecorator.java @@ -0,0 +1,131 @@ +package datadog.trace.instrumentation.openai_java; + +import com.openai.core.ClientOptions; +import com.openai.core.http.Headers; +import datadog.trace.api.Config; +import datadog.trace.api.llmobs.LLMObsContext; +import datadog.trace.api.telemetry.LLMObsMetricCollector; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.ClientDecorator; +import java.util.List; + +public class OpenAiDecorator extends ClientDecorator { + public static final OpenAiDecorator DECORATE = new OpenAiDecorator(); + + public static final String INTEGRATION = "openai"; + public static final String INSTRUMENTATION_NAME = "openai-java"; + public static final CharSequence SPAN_NAME = UTF8BytesString.create("openai.request"); + + public static final String REQUEST_MODEL = "openai.request.model"; + public static final String RESPONSE_MODEL = "openai.response.model"; + public static final String OPENAI_ORGANIZATION_NAME = "openai.organization"; + + private static final CharSequence COMPONENT_NAME = UTF8BytesString.create(INTEGRATION); + + private final boolean llmObsEnabled = Config.get().isLlmObsEnabled(); + + public AgentSpan startSpan(ClientOptions clientOptions) { + AgentSpan span = AgentTracer.startSpan(INSTRUMENTATION_NAME, SPAN_NAME); + afterStart(span); + span.setTag("openai.api_base", clientOptions.baseUrl()); + return span; + } + + public void finishSpan(AgentSpan span, Throwable err) { + try { + if (err != null) { + onError(span, err); + } + DECORATE.beforeFinish(span); + } finally { + span.finish(); + } + } + + @Override + protected String service() { + return null; + } + + @Override + protected String[] instrumentationNames() { + return new String[] {INSTRUMENTATION_NAME}; + } + + @Override + protected CharSequence spanType() { + return InternalSpanTypes.LLMOBS; + } + + @Override + protected CharSequence component() { + return COMPONENT_NAME; + } + + @Override + public AgentSpan afterStart(AgentSpan span) { + if (llmObsEnabled) { + span.setTag("_ml_obs_tag.parent_id", LLMObsContext.parentSpanId()); + } + return super.afterStart(span); + } + + @Override + public AgentSpan beforeFinish(AgentSpan span) { + if (llmObsEnabled) { + Object spanKindTag = span.getTag("_ml_obs_tag.span.kind"); + if (spanKindTag != null) { + String spanKind = spanKindTag.toString(); + boolean isRootSpan = span.getLocalRootSpan() == span; + LLMObsMetricCollector.get() + .recordSpanFinished(INTEGRATION, spanKind, isRootSpan, true, span.isError()); + } + } + return super.beforeFinish(span); + } + + public void withHttpResponse(AgentSpan span, Headers headers) { + if (!llmObsEnabled) { + return; + } + List values = headers.values("openai-organization"); + if (!values.isEmpty()) { + span.setTag(OPENAI_ORGANIZATION_NAME, values.get(0)); + } + setMetricFromHeader( + span, + "openai.organization.ratelimit.requests.limit", + headers, + "x-ratelimit-limit-requests"); + setMetricFromHeader( + span, + "openai.organization.ratelimit.requests.remaining", + headers, + "x-ratelimit-remaining-requests"); + setMetricFromHeader( + span, "openai.organization.ratelimit.tokens.limit", headers, "x-ratelimit-limit-tokens"); + setMetricFromHeader( + span, + "openai.organization.ratelimit.tokens.remaining", + headers, + "x-ratelimit-remaining-tokens"); + } + + private static void setMetricFromHeader( + AgentSpan span, String metric, Headers headers, String header) { + List values = headers.values(header); + if (values.isEmpty()) { + return; + } + String firstHeader = values.get(0); + try { + int value = Integer.parseInt(firstHeader); + span.setMetric(metric, value); + } catch (NumberFormatException ex) { + // ~ + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseDecorator.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseDecorator.java new file mode 100644 index 00000000000..ee46849bfb3 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseDecorator.java @@ -0,0 +1,550 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.REQUEST_MODEL; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.RESPONSE_MODEL; + +import com.openai.core.JsonField; +import com.openai.core.JsonValue; +import com.openai.models.Reasoning; +import com.openai.models.ResponsesModel; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseFunctionToolCall; +import com.openai.models.responses.ResponseInputContent; +import com.openai.models.responses.ResponseInputItem; +import com.openai.models.responses.ResponseOutputItem; +import com.openai.models.responses.ResponseOutputMessage; +import com.openai.models.responses.ResponseOutputText; +import com.openai.models.responses.ResponseReasoningItem; +import com.openai.models.responses.ResponseStreamEvent; +import datadog.json.JsonWriter; +import datadog.trace.api.Config; +import datadog.trace.api.llmobs.LLMObs; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ResponseDecorator { + public static final ResponseDecorator DECORATE = new ResponseDecorator(); + + private static final CharSequence RESPONSES_CREATE = UTF8BytesString.create("createResponse"); + + private final boolean llmObsEnabled = Config.get().isLlmObsEnabled(); + + public void withResponseCreateParams(AgentSpan span, ResponseCreateParams params) { + span.setResourceName(RESPONSES_CREATE); + span.setTag("openai.request.endpoint", "v1/responses"); + span.setTag("openai.request.method", "POST"); + if (!llmObsEnabled) { + return; + } + + span.setTag("_ml_obs_tag.span.kind", Tags.LLMOBS_LLM_SPAN_KIND); + if (params == null) { + return; + } + // Use ResponseCreateParams._model() b/o ResponseCreateParams.model() changed type from + // ResponsesModel to Optional in + // https://github.com/openai/openai-java/commit/87dd64658da6cec7564f3b571e15ec0e2db0660b + String modelName = extractResponseModel(params._model()); + span.setTag(REQUEST_MODEL, modelName); + + List inputMessages = new ArrayList<>(); + + params + .instructions() + .ifPresent( + instructions -> { + inputMessages.add(LLMObs.LLMMessage.from("system", instructions)); + }); + + Optional textOpt = params._input().asString(); // TODO cover with unit tests + if (textOpt.isPresent()) { + inputMessages.add(LLMObs.LLMMessage.from("user", textOpt.get())); + } + + Optional inputOpt = params._input().asKnown(); + if (inputOpt.isPresent()) { + ResponseCreateParams.Input input = inputOpt.get(); + if (input.isText()) { + inputMessages.add(LLMObs.LLMMessage.from("user", input.asText())); + } else if (input.isResponse()) { + List inputItems = input.asResponse(); // TODO cover with unit tests + for (ResponseInputItem item : inputItems) { + LLMObs.LLMMessage message = extractInputItemMessage(item); + if (message != null) { + inputMessages.add(message); + } + } + } + } + + // Handle raw list input (when SDK can't parse into known types) + // This path is tested by "create streaming response with raw json tool input test" + if (inputMessages.isEmpty()) { + try { + Optional rawValueOpt = params._input().asUnknown(); + if (rawValueOpt.isPresent()) { + JsonValue rawValue = rawValueOpt.get(); + Optional> rawListOpt = rawValue.asArray(); + if (rawListOpt.isPresent()) { + for (JsonValue item : rawListOpt.get()) { + LLMObs.LLMMessage message = extractMessageFromRawJson(item); + if (message != null) { + inputMessages.add(message); + } + } + } + } + } catch (Exception e) { + // Ignore parsing errors for raw input + } + } + + if (!inputMessages.isEmpty()) { + span.setTag("_ml_obs_tag.input", inputMessages); + } + + extractReasoningFromParams(params) + .ifPresent(reasoningMap -> span.setTag("_ml_obs_request.reasoning", reasoningMap)); + } + + private LLMObs.LLMMessage extractInputItemMessage(ResponseInputItem item) { + if (item.isMessage()) { + ResponseInputItem.Message message = item.asMessage(); + String role = message.role().asString(); + String content = extractInputMessageContent(message); + return LLMObs.LLMMessage.from(role, content); + } else if (item.isFunctionCall()) { + // Function call is mapped to assistant message with tool_calls + ResponseFunctionToolCall functionCall = item.asFunctionCall(); + LLMObs.ToolCall toolCall = ToolCallExtractor.getToolCall(functionCall); + if (toolCall != null) { + List toolCalls = Collections.singletonList(toolCall); + return LLMObs.LLMMessage.from("assistant", null, toolCalls); + } + } else if (item.isFunctionCallOutput()) { + ResponseInputItem.FunctionCallOutput output = item.asFunctionCallOutput(); + String callId = output.callId(); + String result = FunctionCallOutputExtractor.getOutputAsString(output); + LLMObs.ToolResult toolResult = + LLMObs.ToolResult.from("", "function_call_output", callId, result); + List toolResults = Collections.singletonList(toolResult); + return LLMObs.LLMMessage.fromToolResults("user", toolResults); + } + return null; + } + + private LLMObs.LLMMessage extractMessageFromRawJson(JsonValue jsonValue) { + Optional> objOpt = jsonValue.asObject(); + if (!objOpt.isPresent()) { + return null; + } + + Map obj = objOpt.get(); + JsonValue typeValue = obj.get("type"); + + // Check if it's a function_call + if (typeValue != null) { + Optional typeStr = typeValue.asString(); + if (typeStr.isPresent()) { + String type = typeStr.get(); + + if ("function_call".equals(type)) { + // Extract function call details + JsonValue callIdValue = obj.get("call_id"); + JsonValue nameValue = obj.get("name"); + JsonValue argumentsValue = obj.get("arguments"); + + String callId = null; + String name = null; + String argumentsStr = null; + + if (callIdValue != null) { + Optional opt = callIdValue.asString(); + if (opt.isPresent()) { + callId = opt.get(); + } + } + if (nameValue != null) { + Optional opt = nameValue.asString(); + if (opt.isPresent()) { + name = opt.get(); + } + } + if (argumentsValue != null) { + Optional opt = argumentsValue.asString(); + if (opt.isPresent()) { + argumentsStr = opt.get(); + } + } + + if (callId != null && name != null && argumentsStr != null) { + Map arguments = parseJsonString(argumentsStr); + LLMObs.ToolCall toolCall = + LLMObs.ToolCall.from(name, "function_call", callId, arguments); + return LLMObs.LLMMessage.from("assistant", null, Collections.singletonList(toolCall)); + } + } else if ("function_call_output".equals(type)) { + // Extract function call output + JsonValue callIdValue = obj.get("call_id"); + JsonValue outputValue = obj.get("output"); + + String callId = null; + String output = null; + + if (callIdValue != null) { + Optional opt = callIdValue.asString(); + if (opt.isPresent()) { + callId = opt.get(); + } + } + if (outputValue != null) { + Optional opt = outputValue.asString(); + if (opt.isPresent()) { + output = opt.get(); + } + } + + if (callId != null && output != null) { + LLMObs.ToolResult toolResult = + LLMObs.ToolResult.from("", "function_call_output", callId, output); + return LLMObs.LLMMessage.fromToolResults("user", Collections.singletonList(toolResult)); + } + } + } + } + + // Otherwise, it's a regular message with role and content + JsonValue roleValue = obj.get("role"); + JsonValue contentValue = obj.get("content"); + + String role = null; + String content = null; + + if (roleValue != null) { + Optional opt = roleValue.asString(); + if (opt.isPresent()) { + role = opt.get(); + } + } + if (contentValue != null) { + Optional opt = contentValue.asString(); + if (opt.isPresent()) { + content = opt.get(); + } + } + + if (role != null) { + return LLMObs.LLMMessage.from(role, content); + } + + return null; + } + + private Map parseJsonString(String jsonStr) { + if (jsonStr == null || jsonStr.isEmpty()) { + return Collections.emptyMap(); + } + try { + jsonStr = jsonStr.trim(); + if (!jsonStr.startsWith("{") || !jsonStr.endsWith("}")) { + return Collections.emptyMap(); + } + + Map result = new HashMap<>(); + String content = jsonStr.substring(1, jsonStr.length() - 1).trim(); + + if (content.isEmpty()) { + return result; + } + + // Parse JSON manually, respecting quoted strings + List pairs = splitByCommaRespectingQuotes(content); + + for (String pair : pairs) { + int colonIdx = pair.indexOf(':'); + if (colonIdx > 0) { + String key = pair.substring(0, colonIdx).trim(); + String value = pair.substring(colonIdx + 1).trim(); + + // Remove quotes from key + key = removeQuotes(key); + // Remove quotes from value + value = removeQuotes(value); + + result.put(key, value); + } + } + + return result; + } catch (Exception e) { + return Collections.emptyMap(); + } + } + + private List splitByCommaRespectingQuotes(String str) { + List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + + if (c == '"') { + inQuotes = !inQuotes; + current.append(c); + } else if (c == ',' && !inQuotes) { + result.add(current.toString()); + current = new StringBuilder(); + } else { + current.append(c); + } + } + + if (current.length() > 0) { + result.add(current.toString()); + } + + return result; + } + + private String removeQuotes(String str) { + str = str.trim(); + if (str.startsWith("\"") && str.endsWith("\"") && str.length() >= 2) { + return str.substring(1, str.length() - 1); + } + return str; + } + + private String extractInputMessageContent(ResponseInputItem.Message message) { + StringBuilder contentBuilder = new StringBuilder(); + for (ResponseInputContent content : message.content()) { + if (content.isInputText()) { + contentBuilder.append(content.asInputText().text()); + } + } + String result = contentBuilder.toString(); + return result.isEmpty() ? null : result; + } + + private Optional> extractReasoningFromParams(ResponseCreateParams params) { + JsonField reasoningField = params._reasoning(); + if (reasoningField.isMissing()) { + return Optional.empty(); + } + + Map reasoningMap = new HashMap<>(); + + Optional knownReasoning = reasoningField.asKnown(); + if (knownReasoning.isPresent()) { + Reasoning reasoning = knownReasoning.get(); + reasoning.effort().ifPresent(effort -> reasoningMap.put("effort", effort.asString())); + reasoning.summary().ifPresent(summary -> reasoningMap.put("summary", summary.asString())); + } else { + Optional> rawObject = reasoningField.asObject(); + if (rawObject.isPresent()) { + Map obj = rawObject.get(); + JsonValue effortVal = obj.get("effort"); + if (effortVal != null) { + effortVal.asString().ifPresent(v -> reasoningMap.put("effort", String.valueOf(v))); + } + JsonValue summaryVal = obj.get("summary"); + if (summaryVal == null) { + summaryVal = obj.get("generate_summary"); + } + if (summaryVal != null) { + summaryVal.asString().ifPresent(v -> reasoningMap.put("summary", String.valueOf(v))); + } + } + } + + return reasoningMap.isEmpty() ? Optional.empty() : Optional.of(reasoningMap); + } + + public void withResponse(AgentSpan span, Response response) { + withResponse(span, response, false); + } + + public void withResponseStreamEvents(AgentSpan span, List events) { + if (!llmObsEnabled) { + return; + } + + for (ResponseStreamEvent event : events) { + if (event.isCompleted()) { + Response response = event.asCompleted().response(); + withResponse(span, response, true); + return; + } + if (event.isIncomplete()) { + Response response = event.asIncomplete().response(); + withResponse(span, response, true); + return; + } + } + } + + private void withResponse(AgentSpan span, Response response, boolean stream) { + if (!llmObsEnabled) { + return; + } + + String modelName = extractResponseModel(response._model()); + span.setTag(RESPONSE_MODEL, modelName); + span.setTag("_ml_obs_tag.model_name", modelName); + span.setTag("_ml_obs_tag.model_provider", "openai"); + + List outputMessages = extractResponseOutputMessages(response.output()); + if (!outputMessages.isEmpty()) { + span.setTag("_ml_obs_tag.output", outputMessages); + } + + Map metadata = new HashMap<>(); + + Object reasoningTag = span.getTag("_ml_obs_request.reasoning"); + if (reasoningTag != null) { + metadata.put("reasoning", reasoningTag); + } + + response.maxOutputTokens().ifPresent(v -> metadata.put("max_output_tokens", v)); + response.temperature().ifPresent(v -> metadata.put("temperature", v)); + response.topP().ifPresent(v -> metadata.put("top_p", v)); + + Response.ToolChoice toolChoice = response.toolChoice(); + if (toolChoice.isOptions()) { + metadata.put("tool_choice", toolChoice.asOptions()._value().asString().orElse(null)); + } else if (toolChoice.isTypes()) { + metadata.put("tool_choice", toolChoice.asTypes().type().toString().toLowerCase()); + } else if (toolChoice.isFunction()) { + metadata.put("tool_choice", "function"); + } + + response + .truncation() + .ifPresent( + (Response.Truncation t) -> + metadata.put("truncation", t._value().asString().orElse(null))); + + response + .text() + .ifPresent( + textConfig -> { + textConfig + .format() + .ifPresent( + format -> { + Map textMap = new HashMap<>(); + Map formatMap = new HashMap<>(); + if (format.isText()) { + formatMap.put("type", "text"); + } else if (format.isJsonSchema()) { + formatMap.put("type", "json_schema"); + } else if (format.isJsonObject()) { + formatMap.put("type", "json_object"); + } + textMap.put("format", formatMap); + metadata.put("text", textMap); + }); + }); + + if (stream) { + metadata.put("stream", true); + } + + response + .usage() + .ifPresent( + usage -> { + span.setTag("_ml_obs_metric.input_tokens", usage.inputTokens()); + span.setTag("_ml_obs_metric.output_tokens", usage.outputTokens()); + span.setTag("_ml_obs_metric.total_tokens", usage.totalTokens()); + span.setTag( + "_ml_obs_metric.cache_read_input_tokens", + usage.inputTokensDetails().cachedTokens()); + long reasoningTokens = usage.outputTokensDetails().reasoningTokens(); + metadata.put("reasoning_tokens", reasoningTokens); + }); + + span.setTag("_ml_obs_tag.metadata", metadata); + } + + private List extractResponseOutputMessages(List output) { + List messages = new ArrayList<>(); + + for (ResponseOutputItem item : output) { + if (item.isFunctionCall()) { + ResponseFunctionToolCall functionCall = item.asFunctionCall(); + LLMObs.ToolCall toolCall = ToolCallExtractor.getToolCall(functionCall); + if (toolCall != null) { + List toolCalls = Collections.singletonList(toolCall); + messages.add(LLMObs.LLMMessage.from("assistant", null, toolCalls)); + } + } else if (item.isMessage()) { + ResponseOutputMessage message = item.asMessage(); + String textContent = extractMessageContent(message); + Optional roleOpt = message._role().asString(); + String role = roleOpt.orElse("assistant"); + messages.add(LLMObs.LLMMessage.from(role, textContent)); + } else if (item.isReasoning()) { + ResponseReasoningItem reasoning = item.asReasoning(); + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + if (!reasoning.summary().isEmpty()) { + writer.name("summary").value(reasoning.summary().get(0).text()); + } + reasoning.encryptedContent().ifPresent(v -> writer.name("encrypted_content").value(v)); + writer.name("id").value(reasoning.id()); + writer.endObject(); + messages.add(LLMObs.LLMMessage.from("reasoning", writer.toString())); + } + } + } + return messages; + } + + private String extractMessageContent(ResponseOutputMessage message) { + StringBuilder contentBuilder = new StringBuilder(); + for (ResponseOutputMessage.Content content : message.content()) { + if (content.isOutputText()) { + ResponseOutputText outputText = content.asOutputText(); + contentBuilder.append(outputText.text()); + } + } + String result = contentBuilder.toString(); + return result.isEmpty() ? null : result; + } + + private String extractResponseModel(JsonField model) { + Optional str = model.asString(); + if (str.isPresent()) { + return str.get(); + } + Optional known = model.asKnown(); + if (known.isPresent()) { + ResponsesModel m = known.get(); + if (m.isString()) { + return m.asString(); + } + if (m.isChat()) { + Optional s = m.asChat()._value().asString(); + if (s.isPresent()) { + return s.get(); + } + } + if (m.isOnly()) { + Optional s = m.asOnly()._value().asString(); + if (s.isPresent()) { + return s.get(); + } + } + } + return null; + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseModule.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseModule.java new file mode 100644 index 00000000000..d4c13d102b3 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseModule.java @@ -0,0 +1,34 @@ +package datadog.trace.instrumentation.openai_java; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumenterModule.class) +public class ResponseModule extends InstrumenterModule.Tracing { + public ResponseModule() { + super("openai-java"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".ResponseDecorator", + packageName + ".FunctionCallOutputExtractor", + packageName + ".OpenAiDecorator", + packageName + ".HttpResponseWrapper", + packageName + ".HttpStreamResponseWrapper", + packageName + ".HttpStreamResponseStreamWrapper", + packageName + ".ToolCallExtractor", + packageName + ".ToolCallExtractor$1" + }; + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new ResponseServiceAsyncInstrumentation(), new ResponseServiceInstrumentation()); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseServiceAsyncInstrumentation.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseServiceAsyncInstrumentation.java new file mode 100644 index 00000000000..581c186d877 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseServiceAsyncInstrumentation.java @@ -0,0 +1,100 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.openai.core.ClientOptions; +import com.openai.core.http.HttpResponseFor; +import com.openai.core.http.StreamResponse; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseStreamEvent; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.asm.Advice; + +public class ResponseServiceAsyncInstrumentation + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + @Override + public String instrumentedType() { + return "com.openai.services.async.ResponseServiceAsyncImpl$WithRawResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("create")) + .and(takesArgument(0, named("com.openai.models.responses.ResponseCreateParams"))) + .and(returns(named(CompletableFuture.class.getName()))), + getClass().getName() + "$CreateAdvice"); + + transformer.applyAdvice( + isMethod() + .and(named("createStreaming")) + .and(takesArgument(0, named("com.openai.models.responses.ResponseCreateParams"))) + .and(returns(named(CompletableFuture.class.getName()))), + getClass().getName() + "$CreateStreamingAdvice"); + } + + public static class CreateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ResponseCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ResponseDecorator.DECORATE.withResponseCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) CompletableFuture> future, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || future == null) { + DECORATE.finishSpan(span, err); + } else { + future = + HttpResponseWrapper.wrapFuture(future, span, ResponseDecorator.DECORATE::withResponse); + } + scope.close(); + } + } + + public static class CreateStreamingAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ResponseCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ResponseDecorator.DECORATE.withResponseCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) + CompletableFuture>> future, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || future == null) { + DECORATE.finishSpan(span, err); + } else { + future = + HttpStreamResponseWrapper.wrapFuture( + future, span, ResponseDecorator.DECORATE::withResponseStreamEvents); + } + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseServiceInstrumentation.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseServiceInstrumentation.java new file mode 100644 index 00000000000..61d96823a8b --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ResponseServiceInstrumentation.java @@ -0,0 +1,99 @@ +package datadog.trace.instrumentation.openai_java; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.openai.core.ClientOptions; +import com.openai.core.http.HttpResponseFor; +import com.openai.core.http.StreamResponse; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseStreamEvent; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import net.bytebuddy.asm.Advice; + +public class ResponseServiceInstrumentation + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + @Override + public String instrumentedType() { + return "com.openai.services.blocking.ResponseServiceImpl$WithRawResponseImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("create")) + .and(takesArgument(0, named("com.openai.models.responses.ResponseCreateParams"))) + .and(returns(named("com.openai.core.http.HttpResponseFor"))), + getClass().getName() + "$CreateAdvice"); + + transformer.applyAdvice( + isMethod() + .and(named("createStreaming")) + .and(takesArgument(0, named("com.openai.models.responses.ResponseCreateParams"))) + .and(returns(named("com.openai.core.http.HttpResponseFor"))), + getClass().getName() + "$CreateStreamingAdvice"); + } + + public static class CreateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ResponseCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ResponseDecorator.DECORATE.withResponseCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) HttpResponseFor response, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || response == null) { + DECORATE.finishSpan(span, err); + } else { + response = + HttpResponseWrapper.wrap(response, span, ResponseDecorator.DECORATE::withResponse); + } + scope.close(); + } + } + + public static class CreateStreamingAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope enter( + @Advice.Argument(0) final ResponseCreateParams params, + @Advice.FieldValue("clientOptions") ClientOptions clientOptions) { + AgentSpan span = DECORATE.startSpan(clientOptions); + ResponseDecorator.DECORATE.withResponseCreateParams(span, params); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) + HttpResponseFor> response, + @Advice.Thrown final Throwable err) { + AgentSpan span = scope.span(); + if (err != null || response == null) { + DECORATE.finishSpan(span, err); + } else { + response = + HttpStreamResponseWrapper.wrap( + response, span, ResponseDecorator.DECORATE::withResponseStreamEvents); + } + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ToolCallExtractor.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ToolCallExtractor.java new file mode 100644 index 00000000000..357c73de0aa --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/main/java/datadog/trace/instrumentation/openai_java/ToolCallExtractor.java @@ -0,0 +1,75 @@ +package datadog.trace.instrumentation.openai_java; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.responses.ResponseFunctionToolCall; +import datadog.trace.api.llmobs.LLMObs; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ToolCallExtractor { + private static final Logger log = LoggerFactory.getLogger(ToolCallExtractor.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final TypeReference> MAP_TYPE_REF = + new TypeReference>() {}; + + public static LLMObs.ToolCall getToolCall(ChatCompletionMessageToolCall toolCall) { + Optional functionToolCallOpt = toolCall.function(); + if (!functionToolCallOpt.isPresent()) { + return null; + } + try { + ChatCompletionMessageFunctionToolCall functionToolCall = functionToolCallOpt.get(); + String toolId = functionToolCall.id(); + ChatCompletionMessageFunctionToolCall.Function function = functionToolCall.function(); + String name = function.name(); + String argumentsJson = function.arguments(); + + String type = "function"; + Optional typeOpt = functionToolCall._type().asString(); + if (typeOpt.isPresent()) { + type = typeOpt.get(); + } + + Map arguments = parseArguments(argumentsJson); + return LLMObs.ToolCall.from(name, type, toolId, arguments); + } catch (Exception e) { + log.debug("Failed to extract tool call information", e); + } + return null; + } + + public static LLMObs.ToolCall getToolCall(ResponseFunctionToolCall functionCall) { + try { + String name = functionCall.name(); + String callId = functionCall.callId(); + String argumentsJson = functionCall.arguments(); + + String type = "function_call"; + Optional typeOpt = functionCall._type().asString(); + if (typeOpt.isPresent()) { + type = typeOpt.get(); + } + + Map arguments = parseArguments(argumentsJson); + return LLMObs.ToolCall.from(name, type, callId, arguments); + } catch (Exception e) { + log.debug("Failed to extract tool call information", e); + } + return null; + } + + private static Map parseArguments(String argumentsJson) { + try { + return MAPPER.readValue(argumentsJson, MAP_TYPE_REF); + } catch (Exception e) { + log.debug("Failed to parse tool call arguments as JSON: {}", argumentsJson, e); + return Collections.singletonMap("value", argumentsJson); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/ChatCompletionServiceTest.groovy b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/ChatCompletionServiceTest.groovy new file mode 100644 index 00000000000..019467b7cc7 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/ChatCompletionServiceTest.groovy @@ -0,0 +1,264 @@ +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace +import static datadog.trace.agent.test.utils.TraceUtils.runnableUnderTrace + +import com.openai.core.http.AsyncStreamResponse +import com.openai.core.http.HttpResponseFor +import com.openai.core.http.StreamResponse +import com.openai.models.chat.completions.ChatCompletion +import com.openai.models.chat.completions.ChatCompletionChunk +import com.openai.models.completions.Completion +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.llmobs.LLMObs +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.openai_java.OpenAiDecorator +import java.util.concurrent.CompletableFuture +import java.util.stream.Stream + +class ChatCompletionServiceTest extends OpenAiTest { + + // TODO add a multi-choice response tests + + def "create chat/completion test"() { + ChatCompletion resp = runUnderTrace("parent") { + openAiClient.chat().completions().create(params) + } + + expect: + resp != null + and: + assertChatCompletionTrace(false) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create chat/completion test withRawResponse"() { + HttpResponseFor resp = runUnderTrace("parent") { + openAiClient.chat().withRawResponse().completions().create(params) + } + + expect: + resp.statusCode() == 200 + resp.parse().valid // force response parsing, so it sets all the tags + and: + assertChatCompletionTrace(false) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create streaming chat/completion test"() { + runnableUnderTrace("parent") { + StreamResponse streamCompletion = openAiClient.chat().completions().createStreaming(params) + try (Stream stream = streamCompletion.stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertChatCompletionTrace(true) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create streaming chat/completion test withRawResponse"() { + runnableUnderTrace("parent") { + HttpResponseFor> streamCompletion = openAiClient.chat().completions().withRawResponse().createStreaming(params) + try (Stream stream = streamCompletion.parse().stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertChatCompletionTrace(true) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create async chat/completion test"() { + CompletableFuture completionFuture = runUnderTrace("parent") { + openAiClient.async().chat().completions().create(params) + } + + completionFuture.get() + + expect: + assertChatCompletionTrace(false) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create async chat/completion test withRawResponse"() { + CompletableFuture> completionFuture = runUnderTrace("parent") { + openAiClient.async().chat().completions().withRawResponse().create(params) + } + + def resp = completionFuture.get() + resp.parse().valid // force response parsing, so it sets all the tags + + expect: + assertChatCompletionTrace(false) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create streaming async chat/completion test"() { + AsyncStreamResponse asyncResp = runUnderTrace("parent") { + openAiClient.async().chat().completions().createStreaming(params) + } + asyncResp.subscribe { + // consume completions + } + asyncResp.onCompleteFuture().get() + expect: + assertChatCompletionTrace(true) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create streaming async chat/completion test withRawResponse"() { + CompletableFuture>> future = runUnderTrace("parent") { + openAiClient.async().chat().completions().withRawResponse().createStreaming(params) + } + HttpResponseFor> resp = future.get() + try (Stream stream = resp.parse().stream()) { + stream.forEach { + // consume the stream + } + } + expect: + resp.statusCode() == 200 + assertChatCompletionTrace(true) + + where: + params << [chatCompletionCreateParams(false), chatCompletionCreateParams(true)] + } + + def "create chat/completion test with tool calls"() { + runUnderTrace("parent") { + openAiClient.chat().completions().create(chatCompletionCreateParamsWithTools()) + } + + expect: + List outputTag = [] + assertChatCompletionTrace(false, outputTag) + and: + outputTag.size() == 1 + LLMObs.LLMMessage outputMsg = outputTag.get(0) + outputMsg.toolCalls.size() == 1 + def toolcall = outputMsg.toolCalls.get(0) + toolcall.name == "extract_student_info" + toolcall.toolId instanceof String + toolcall.type == "function" + toolcall.arguments == [ + name: 'David Nguyen', + major: 'computer science', + school: 'Stanford University', + grades: 3.8, + clubs: ['Chess Club', 'South Asian Student Association'] + ] + } + + def "create streaming chat/completion test with tool calls"() { + runnableUnderTrace("parent") { + StreamResponse streamCompletion = openAiClient.chat().completions().createStreaming(chatCompletionCreateParamsWithTools()) + try (Stream stream = streamCompletion.stream()) { + stream.forEach { + chunk -> + // chunks.add(chunk) + } + } + } + + expect: + List outputTag = [] + assertChatCompletionTrace(true, outputTag) + and: + outputTag.size() == 1 + LLMObs.LLMMessage outputMsg = outputTag.get(0) + outputMsg.toolCalls.size() == 1 + def toolcall = outputMsg.toolCalls.get(0) + toolcall.name == "extract_student_info" + toolcall.toolId instanceof String + toolcall.type == "function" + toolcall.arguments == [ + name: 'David Nguyen', + major: 'computer science', + school: 'Stanford University', + grades: 3.8, + clubs: ['Chess Club', 'South Asian Student Association'] + ] + } + + private void assertChatCompletionTrace(boolean isStreaming) { + assertChatCompletionTrace(isStreaming, null) + } + + private void assertChatCompletionTrace(boolean isStreaming, List outputTagsOut) { + assertTraces(1) { + trace(3) { + sortSpansByStart() + span(0) { + operationName "parent" + parent() + errored false + } + span(1) { + operationName "openai.request" + resourceName "createChatCompletion" + childOf span(0) + errored false + spanType DDSpanTypes.LLMOBS + tags { + "_ml_obs_tag.span.kind" "llm" + "_ml_obs_tag.model_provider" "openai" + "_ml_obs_tag.model_name" String + "_ml_obs_tag.metadata" Map + "_ml_obs_tag.input" List + "_ml_obs_tag.output" List + def outputTags = tag("_ml_obs_tag.output") + if (outputTagsOut != null && outputTags != null) { + outputTagsOut.addAll(outputTags) + } + if (!isStreaming) { + // streamed completions missing usage data + "_ml_obs_metric.input_tokens" Long + "_ml_obs_metric.output_tokens" Long + "_ml_obs_metric.total_tokens" Long + } + "_ml_obs_tag.parent_id" "undefined" + "openai.request.method" "POST" + "openai.request.endpoint" "v1/chat/completions" + "openai.api_base" openAiBaseApi + "openai.organization.ratelimit.requests.limit" 30000 + "openai.organization.ratelimit.requests.remaining" Integer + "openai.organization.ratelimit.tokens.limit" 150000000 + "openai.organization.ratelimit.tokens.remaining" Integer + "$OpenAiDecorator.REQUEST_MODEL" "gpt-4o-mini" + "$OpenAiDecorator.RESPONSE_MODEL" "gpt-4o-mini-2024-07-18" + "$OpenAiDecorator.OPENAI_ORGANIZATION_NAME" "datadog-staging" + "$Tags.COMPONENT" "openai" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + defaultTags() + } + } + span(2) { + operationName "okhttp.request" + resourceName "POST /v1/chat/completions" + childOf span(1) + errored false + spanType "http" + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/CompletionServiceTest.groovy b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/CompletionServiceTest.groovy new file mode 100644 index 00000000000..6ae99d159b8 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/CompletionServiceTest.groovy @@ -0,0 +1,193 @@ +import static datadog.trace.agent.test.utils.TraceUtils.runnableUnderTrace +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +import com.openai.core.http.AsyncStreamResponse +import com.openai.core.http.HttpResponseFor +import com.openai.core.http.StreamResponse +import com.openai.models.completions.Completion +import datadog.trace.api.DDSpanTypes +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.openai_java.OpenAiDecorator +import java.util.concurrent.CompletableFuture +import java.util.stream.Stream + +class CompletionServiceTest extends OpenAiTest { + + def "create completion test"() { + runUnderTrace("parent") { + openAiClient.completions().create(params) + } + + expect: + assertCompletionTrace(false) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + def "create completion test withRawResponse"() { + HttpResponseFor resp = runUnderTrace("parent") { + openAiClient.withRawResponse().completions().create(params) + } + + expect: + resp.statusCode() == 200 + resp.parse().valid // force response parsing, so it sets all the tags + and: + assertCompletionTrace(false) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + def "create streaming completion test"() { + runnableUnderTrace("parent") { + StreamResponse streamCompletion = openAiClient.completions().createStreaming(params) + try (Stream stream = streamCompletion.stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertCompletionTrace(true) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + def "create streaming completion test withRawResponse"() { + runnableUnderTrace("parent") { + HttpResponseFor> streamCompletion = openAiClient.completions().withRawResponse().createStreaming(params) + try (Stream stream = streamCompletion.parse().stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertCompletionTrace(true) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + def "create async completion test"() { + CompletableFuture completionFuture = runUnderTrace("parent") { + openAiClient.async().completions().create(params) + } + + completionFuture.get() + + expect: + assertCompletionTrace(false) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + def "create async completion test withRawResponse"() { + CompletableFuture> completionFuture = runUnderTrace("parent") { + openAiClient.async().completions().withRawResponse().create(params) + } + + def resp = completionFuture.get() + resp.parse().valid // force response parsing, so it sets all the tags + + expect: + assertCompletionTrace(false) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + def "create streaming async completion test"() { + AsyncStreamResponse asyncResp = runUnderTrace("parent") { + openAiClient.async().completions().createStreaming(params) + } + asyncResp.subscribe { + // consume completions + } + asyncResp.onCompleteFuture().get() + expect: + assertCompletionTrace(true) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + def "create streaming async completion test withRawResponse"() { + CompletableFuture>> future = runUnderTrace("parent") { + openAiClient.async().completions().withRawResponse().createStreaming(params) + } + HttpResponseFor> resp = future.get() + try (Stream stream = resp.parse().stream()) { + stream.forEach { + // consume the stream + } + } + expect: + resp.statusCode() == 200 + assertCompletionTrace(true) + + where: + params << [completionCreateParams(true), completionCreateParams(false)] + } + + private void assertCompletionTrace(boolean isStreaming) { + assertTraces(1) { + trace(3) { + sortSpansByStart() + span(0) { + operationName "parent" + parent() + errored false + } + span(1) { + operationName "openai.request" + resourceName "createCompletion" + childOf span(0) + errored false + spanType DDSpanTypes.LLMOBS + tags { + "_ml_obs_tag.span.kind" "llm" + "_ml_obs_tag.model_provider" "openai" + "_ml_obs_tag.model_name" String + "_ml_obs_tag.metadata" Map + "_ml_obs_tag.input" List + "_ml_obs_tag.output" List + if (!isStreaming) { + // streamed completions missing usage data + "_ml_obs_metric.input_tokens" Long + "_ml_obs_metric.output_tokens" Long + "_ml_obs_metric.total_tokens" Long + } + "_ml_obs_tag.parent_id" "undefined" + "openai.request.method" "POST" + "openai.request.endpoint" "v1/completions" + "openai.api_base" openAiBaseApi + "openai.organization.ratelimit.requests.limit" 3500 + "openai.organization.ratelimit.requests.remaining" Integer + "openai.organization.ratelimit.tokens.limit" 90000 + "openai.organization.ratelimit.tokens.remaining" Integer + "$OpenAiDecorator.REQUEST_MODEL" "gpt-3.5-turbo-instruct" + "$OpenAiDecorator.RESPONSE_MODEL" "gpt-3.5-turbo-instruct:20230824-v2" + "$OpenAiDecorator.OPENAI_ORGANIZATION_NAME" "datadog-staging" + "$Tags.COMPONENT" "openai" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + defaultTags() + } + } + span(2) { + operationName "okhttp.request" + resourceName "POST /v1/completions" + childOf span(1) + errored false + spanType "http" + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/EmbeddingServiceTest.groovy b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/EmbeddingServiceTest.groovy new file mode 100644 index 00000000000..14bc9485f3b --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/EmbeddingServiceTest.groovy @@ -0,0 +1,90 @@ +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +import com.openai.core.http.HttpResponseFor +import com.openai.models.embeddings.CreateEmbeddingResponse +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.llmobs.LLMObs +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.openai_java.OpenAiDecorator + +class EmbeddingServiceTest extends OpenAiTest { + + def "create embedding test"() { + CreateEmbeddingResponse resp = runUnderTrace("parent") { + openAiClient.embeddings().create(params) + } + + expect: + resp != null + and: + assertEmbeddingTrace() + + where: + params << [embeddingCreateParams(false), embeddingCreateParams(true)] + } + + def "create embedding test withRawResponse"() { + HttpResponseFor resp = runUnderTrace("parent") { + openAiClient.embeddings().withRawResponse().create(params) + } + + expect: + resp != null + and: + resp.parse().valid // force response parsing, so it sets all the tags + and: + assertEmbeddingTrace() + + where: + params << [embeddingCreateParams(false), embeddingCreateParams(true)] + } + + private void assertEmbeddingTrace() { + assertTraces(1) { + trace(3) { + sortSpansByStart() + span(0) { + operationName "parent" + parent() + errored false + } + span(1) { + operationName "openai.request" + resourceName "createEmbedding" + childOf span(0) + errored false + spanType DDSpanTypes.LLMOBS + tags { + "_ml_obs_tag.span.kind" "embedding" + "_ml_obs_tag.model_provider" "openai" + "_ml_obs_tag.model_name" "text-embedding-ada-002-v2" + "_ml_obs_tag.input" List + "_ml_obs_tag.metadata" Map + "_ml_obs_tag.output" "[1 embedding(s) returned with size 1536]" + "_ml_obs_tag.parent_id" "undefined" + "openai.request.method" "POST" + "openai.request.endpoint" "v1/embeddings" + "openai.api_base" openAiBaseApi + "openai.organization.ratelimit.requests.limit" 10000 + "openai.organization.ratelimit.requests.remaining" Integer + "openai.organization.ratelimit.tokens.limit" 10000000 + "openai.organization.ratelimit.tokens.remaining" Integer + "$OpenAiDecorator.REQUEST_MODEL" "text-embedding-ada-002" + "$OpenAiDecorator.RESPONSE_MODEL" "text-embedding-ada-002-v2" + "$OpenAiDecorator.OPENAI_ORGANIZATION_NAME" "datadog-staging" + "$Tags.COMPONENT" "openai" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + defaultTags() + } + } + span(2) { + operationName "okhttp.request" + resourceName "POST /v1/embeddings" + childOf span(1) + errored false + spanType "http" + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/OpenAiTest.groovy b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/OpenAiTest.groovy new file mode 100644 index 00000000000..ec72792bc7c --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/OpenAiTest.groovy @@ -0,0 +1,297 @@ +import com.google.common.base.Charsets +import com.google.common.base.Strings +import com.openai.client.OpenAIClient +import com.openai.client.okhttp.OkHttpClient +import com.openai.client.okhttp.OpenAIOkHttpClient +import com.openai.core.ClientOptions +import com.openai.credential.BearerTokenCredential +import com.openai.core.JsonValue +import com.openai.models.ChatModel +import com.openai.models.FunctionDefinition +import com.openai.models.FunctionParameters +import com.openai.models.Reasoning +import com.openai.models.ReasoningEffort +import com.openai.models.chat.completions.ChatCompletionCreateParams +import com.openai.models.chat.completions.ChatCompletionFunctionTool +import com.openai.models.completions.CompletionCreateParams +import com.openai.models.embeddings.EmbeddingCreateParams +import com.openai.models.embeddings.EmbeddingModel +import com.openai.models.responses.ResponseCreateParams +import com.openai.models.responses.ResponseFunctionToolCall +import com.openai.models.responses.ResponseIncludable +import com.openai.models.responses.ResponseInputItem +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.agent.test.server.http.TestHttpServer +import datadog.trace.api.config.LlmObsConfig +import datadog.trace.core.util.LRUCache +import java.nio.file.Path +import java.nio.file.Paths +import spock.lang.AutoCleanup +import spock.lang.Shared + +abstract class OpenAiTest extends InstrumentationSpecification { + + // openai token - will use real openai backend and record request/responses to use later in the mock mode + // empty or null - will use mockOpenAiBackend and read recorded request/responses + static final String OPENAI_TOKEN = "" + + private static final Path RECORDS_DIR = Paths.get("src/test/resources/http-records") + private static final String API_VERSION = "v1" + + @AutoCleanup + @Shared + OpenAIClient openAiClient + + @Shared + def openAiBaseApi + + @AutoCleanup + @Shared + def mockOpenAiBackend = TestHttpServer.httpServer { + LRUCache cache = new LRUCache(8) + handlers { + prefix("/$API_VERSION/") { + def requestBody = request.text + def recFile = RequestResponseRecord.requestToFileName(request.method, requestBody.getBytes(Charsets.UTF_8)) + def rec = cache.get(recFile) + if (rec == null) { + String path = request.path + def subpath = path.substring(API_VERSION.length() + 2) + def recsDir = RECORDS_DIR.resolve(subpath) + def recPath = recsDir.resolve(recFile) + if (!recPath.toFile().exists()) { + throw new RuntimeException("The record file: '" + recFile + "' is NOT found at " + RECORDS_DIR + ". Set OpenAiTest.OPENAI_TOKEN to make a real request and store the record.") + } else { + rec = RequestResponseRecord.read(recPath) + cache.put(recFile, rec) + } + } + def resp = response + resp.status(rec.status) + rec.headers.forEach(resp::addHeader) + resp.send(rec.body) + } + } + } + + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig(LlmObsConfig.LLMOBS_ENABLED, "true") + } + + def setupSpec() { + if (Strings.isNullOrEmpty(OPENAI_TOKEN)) { + // mock backend uses request/response records + OpenAIOkHttpClient.Builder b = OpenAIOkHttpClient.builder() + openAiBaseApi = "${mockOpenAiBackend.address.toURL()}/$API_VERSION" + b.baseUrl(openAiBaseApi) + b.credential(BearerTokenCredential.create("")) + openAiClient = b.build() + } else { + // real openai backend, with custom httpClient to capture and save request/response records + ClientOptions.Builder clientOptions = ClientOptions.builder() + OkHttpClient.Builder httpClient = OkHttpClient.builder() + openAiBaseApi = ClientOptions.PRODUCTION_URL + httpClientUrlIfExists(httpClient, openAiBaseApi) + clientOptions.baseUrl(openAiBaseApi) + clientOptions.credential(BearerTokenCredential.create(OPENAI_TOKEN)) + clientOptions.httpClient(new OpenAiHttpClientForTests(httpClient.build(), RECORDS_DIR)) + openAiClient = createOpenAiClient(clientOptions.build()) + } + } + + void httpClientUrlIfExists(OkHttpClient.Builder httpClient, String url) { + try { + def method = httpClient.getClass().getMethod("baseUrl", String) + method.invoke(httpClient, url) + } catch (NoSuchMethodException e) { + // method exists and mandatory only prior to v3.0.0 + } + } + + OpenAIClient createOpenAiClient(ClientOptions clientOptions) { + // use reflection to set private httpClient via clientOptions + def clazz = Class.forName("com.openai.client.OpenAIClientImpl") + def constructor = clazz.constructors[0] + constructor.accessible = true + constructor.newInstance(clientOptions) as OpenAIClient + } + + CompletionCreateParams completionCreateParams(boolean json) { + if (json) { + CompletionCreateParams.builder() + .model(CompletionCreateParams.Model.GPT_3_5_TURBO_INSTRUCT) + .prompt("Tell me a story about building the best SDK!") + .build() + } else { + CompletionCreateParams.builder() + .model("gpt-3.5-turbo-instruct") + .prompt("Tell me a story about building the best SDK!") + .build() + } + } + + ChatCompletionCreateParams chatCompletionCreateParams(boolean json) { + if (json) { + ChatCompletionCreateParams.builder() + .model("gpt-4o-mini") + .addSystemMessage("") + .addUserMessage("") + .build() + } else { + ChatCompletionCreateParams.builder() + .model(ChatModel.GPT_4O_MINI) + .addSystemMessage("") + .addUserMessage("") + .build() + } + } + + EmbeddingCreateParams embeddingCreateParams(boolean json) { + if (json) { + EmbeddingCreateParams.builder() + .model("text-embedding-ada-002") + .input("hello world") + .build() + } else { + EmbeddingCreateParams.builder() + .model(EmbeddingModel.TEXT_EMBEDDING_ADA_002) + .input("hello world") + .build() + } + } + + ResponseCreateParams responseCreateParams(boolean json) { + if (json) { + ResponseCreateParams.builder() + .model("gpt-3.5-turbo") + .input("Do not continue the Evan Li slander!") + .build() + } else { + ResponseCreateParams.builder() + .model(ChatModel.GPT_3_5_TURBO) + .input("Do not continue the Evan Li slander!") + .build() + } + } + + ResponseCreateParams responseCreateParamsWithMaxOutputTokens(boolean json) { + if (json) { + ResponseCreateParams.builder() + .model("gpt-3.5-turbo") + .input("Do not continue the Evan Li slander!") + .maxOutputTokens(30) + .build() + } else { + ResponseCreateParams.builder() + .model(ChatModel.GPT_3_5_TURBO) + .input("Do not continue the Evan Li slander!") + .maxOutputTokens(30) + .build() + } + } + + ResponseCreateParams responseCreateParamsWithReasoning(boolean json) { + if (json) { + ResponseCreateParams.builder() + .model("o4-mini") + .input("If one plus a number is 10, what is the number?") + .include(Collections.singletonList(ResponseIncludable.of("reasoning.encrypted_content"))) + .reasoning(JsonValue.from([effort: "medium", summary: "detailed"])) + .build() + } else { + ResponseCreateParams.builder() + .model(ChatModel.O4_MINI) + .input("If one plus a number is 10, what is the number?") + .include(Collections.singletonList(ResponseIncludable.REASONING_ENCRYPTED_CONTENT)) + .reasoning(Reasoning.builder().effort(ReasoningEffort.MEDIUM).summary(Reasoning.Summary.DETAILED).build()) + .build() + } + } + + ChatCompletionCreateParams chatCompletionCreateParamsWithTools() { + ChatCompletionCreateParams.builder() + .model(ChatModel.GPT_4O_MINI) + .addUserMessage("""David Nguyen is a sophomore majoring in computer science at Stanford University and has a GPA of 3.8. +David is an active member of the university's Chess Club and the South Asian Student Association. +He hopes to pursue a career in software engineering after graduating.""") + .addTool(ChatCompletionFunctionTool.builder() + .function(FunctionDefinition.builder() + .name("extract_student_info") + .description("Get the student information from the body of the input text") + .parameters(FunctionParameters.builder() + .putAdditionalProperty("type", JsonValue.from("object")) + .putAdditionalProperty("properties", JsonValue.from([ + name: [type: "string", description: "Name of the person"], + major: [type: "string", description: "Major subject."], + school: [type: "string", description: "The university name."], + grades: [type: "integer", description: "GPA of the student."], + clubs: [ + type: "array", + description: "School clubs for extracurricular activities. ", + items: [type: "string", description: "Name of School Club"] + ] + ])) + .build()) + .build()) + .build()) + .build() + } + + ResponseCreateParams responseCreateParamsWithToolInput(boolean json) { + if (json) { + def rawInputJson = [ + [ + role: "user", + content: "What's the weather like in San Francisco?" + ], + [ + type: "function_call", + call_id: "call_123", + name: "get_weather", + arguments: '{"location": "San Francisco, CA"}' + ], + [ + type: "function_call_output", + call_id: "call_123", + output: '{"temperature": "72°F", "conditions": "sunny", "humidity": "65%"}' + ] + ] + + ResponseCreateParams.builder() + .model("gpt-4.1") + .input(com.openai.core.JsonValue.from(rawInputJson)) + .temperature(0.1d) + .build() + } else { + def functionCall = ResponseFunctionToolCall.builder() + .callId("call_123") + .name("get_weather") + .arguments('{"location": "San Francisco, CA"}') + .id("fc_123") + .status(ResponseFunctionToolCall.Status.COMPLETED) + .build() + + def inputItems = [ + ResponseInputItem.ofMessage(ResponseInputItem.Message.builder() + .role(ResponseInputItem.Message.Role.USER) + .addInputTextContent("What's the weather like in San Francisco?") + .build()), + ResponseInputItem.ofFunctionCall(functionCall), + ResponseInputItem.ofFunctionCallOutput(ResponseInputItem.FunctionCallOutput.builder() + .callId("call_123") + .output('{"temperature": "72°F", "conditions": "sunny", "humidity": "65%"}') + .build()) + ] + + ResponseCreateParams.builder() + .model(ChatModel.GPT_4_1) + .input(ResponseCreateParams.Input.ofResponse(inputItems)) + .temperature(0.1d) + .build() + } + } +} + + diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/ResponseServiceTest.groovy b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/ResponseServiceTest.groovy new file mode 100644 index 00000000000..874e6201d5e --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/groovy/ResponseServiceTest.groovy @@ -0,0 +1,246 @@ +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace +import static datadog.trace.agent.test.utils.TraceUtils.runnableUnderTrace + +import com.openai.core.http.AsyncStreamResponse +import com.openai.core.http.HttpResponseFor +import com.openai.core.http.StreamResponse +import com.openai.models.responses.Response +import com.openai.models.responses.ResponseStreamEvent +import datadog.trace.api.DDSpanTypes +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.openai_java.OpenAiDecorator +import java.util.concurrent.CompletableFuture +import java.util.stream.Stream + +class ResponseServiceTest extends OpenAiTest { + + def "create response test"() { + Response resp = runUnderTrace("parent") { + openAiClient.responses().create(params) + } + + expect: + resp != null + and: + assertResponseTrace(false, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + params << [responseCreateParams(false), responseCreateParams(true)] + } + + def "create response test withRawResponse"() { + HttpResponseFor resp = runUnderTrace("parent") { + openAiClient.responses().withRawResponse().create(params) + } + + expect: + resp.statusCode() == 200 + resp.parse().valid // force response parsing, so it sets all the tags + and: + assertResponseTrace(false, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + params << [responseCreateParams(false), responseCreateParams(true)] + } + + def "create streaming response test (#scenario)"() { + runnableUnderTrace("parent") { + StreamResponse streamResponse = openAiClient.responses().createStreaming(params) + try (Stream stream = streamResponse.stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertResponseTrace(true, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + scenario | params + "complete" | responseCreateParams(false) + "complete" | responseCreateParams(true) + "incomplete" | responseCreateParamsWithMaxOutputTokens(false) + "incomplete" | responseCreateParamsWithMaxOutputTokens(true) + } + + def "create streaming response test (reasoning)"() { + runnableUnderTrace("parent") { + StreamResponse streamResponse = openAiClient.responses().createStreaming(responseCreateParams) + try (Stream stream = streamResponse.stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertResponseTrace(true, "o4-mini", "o4-mini-2025-04-16", [effort: "medium", summary: "detailed"]) + + where: + responseCreateParams << [responseCreateParamsWithReasoning(false), responseCreateParamsWithReasoning(true)] + } + + def "create streaming response test withRawResponse"() { + runnableUnderTrace("parent") { + HttpResponseFor> streamResponse = openAiClient.responses().withRawResponse().createStreaming(params) + try (Stream stream = streamResponse.parse().stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertResponseTrace(true, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + params << [responseCreateParams(false), responseCreateParams(true)] + } + + def "create async response test"() { + CompletableFuture responseFuture = runUnderTrace("parent") { + openAiClient.async().responses().create(params) + } + + responseFuture.get() + + expect: + assertResponseTrace(false, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + params << [responseCreateParams(false), responseCreateParams(true)] + } + + def "create async response test withRawResponse"() { + CompletableFuture> responseFuture = runUnderTrace("parent") { + openAiClient.async().responses().withRawResponse().create(params) + } + + def resp = responseFuture.get() + resp.parse().valid // force response parsing, so it sets all the tags + + expect: + assertResponseTrace(false, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + params << [responseCreateParams(false), responseCreateParams(true)] + } + + def "create streaming async response test"() { + AsyncStreamResponse asyncResp = runUnderTrace("parent") { + openAiClient.async().responses().createStreaming(params) + } + asyncResp.subscribe { + // consume responses + } + asyncResp.onCompleteFuture().get() + expect: + assertResponseTrace(true, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + params << [responseCreateParams(false), responseCreateParams(true)] + } + + def "create streaming async response test withRawResponse"() { + CompletableFuture>> future = runUnderTrace("parent") { + openAiClient.async().responses().withRawResponse().createStreaming(params) + } + HttpResponseFor> resp = future.get() + try (Stream stream = resp.parse().stream()) { + stream.forEach { + // consume the stream + } + } + expect: + resp.statusCode() == 200 + assertResponseTrace(true, "gpt-3.5-turbo", "gpt-3.5-turbo-0125", null) + + where: + params << [responseCreateParams(false), responseCreateParams(true)] + } + + def "create streaming response with tool input test"() { + // Tests the strongly-typed path: ResponseInputItem objects → params._input().asKnown() + runnableUnderTrace("parent") { + StreamResponse streamResponse = openAiClient.responses().createStreaming(responseCreateParamsWithToolInput(true)) // TODO + try (Stream stream = streamResponse.stream()) { + stream.forEach { + // consume the stream + } + } + } + + expect: + assertResponseTrace(true, "gpt-4.1", "gpt-4.1-2025-04-14", null) + } + + // NOTE: responseCreateParamsWithToolInput(true) creates raw JSON via JsonValue.from() + // This exercises the asUnknown() → asArray() parsing path in ResponseDecorator + // However, it cannot be unit tested here because: + // 1. Raw JSON serializes differently than typed objects + // 2. No matching HTTP recording exists in the mock backend + // 3. The test would fail with InternalServerException + // + // This path IS tested by the shared integration test: + // llm-obs/test/test_openai.py::test_responses_create_tool_input + // which represents the real cross-language use case (Python → Java test server) + + private void assertResponseTrace(boolean isStreaming, String reqModel, String respModel, Map reasoning) { + assertTraces(1) { + trace(3) { + sortSpansByStart() + span(0) { + operationName "parent" + parent() + errored false + } + span(1) { + operationName "openai.request" + resourceName "createResponse" + childOf span(0) + errored false + spanType DDSpanTypes.LLMOBS + tags { + "_ml_obs_tag.span.kind" "llm" + "_ml_obs_tag.model_provider" "openai" + "_ml_obs_tag.model_name" String + "_ml_obs_tag.metadata" Map + "_ml_obs_tag.input" List + "_ml_obs_tag.output" List // TODO capture to validate tool calls + "_ml_obs_metric.input_tokens" Long + "_ml_obs_metric.output_tokens" Long + "_ml_obs_metric.total_tokens" Long + "_ml_obs_metric.cache_read_input_tokens" Long + "_ml_obs_tag.parent_id" "undefined" + if (reasoning != null) { + "_ml_obs_request.reasoning" reasoning + } + "openai.request.method" "POST" + "openai.request.endpoint" "v1/responses" + "openai.api_base" openAiBaseApi + "$OpenAiDecorator.RESPONSE_MODEL" respModel + if (!isStreaming) { + "openai.organization.ratelimit.requests.limit" 10000 + "openai.organization.ratelimit.requests.remaining" Integer + "openai.organization.ratelimit.tokens.limit" 50000000 + "openai.organization.ratelimit.tokens.remaining" Integer + } + "$OpenAiDecorator.OPENAI_ORGANIZATION_NAME" "datadog-staging" + "$OpenAiDecorator.REQUEST_MODEL" reqModel + "$Tags.COMPONENT" "openai" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + defaultTags() + } + } + span(2) { + operationName "okhttp.request" + resourceName "POST /v1/responses" + childOf span(1) + errored false + spanType "http" + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/java/OpenAiHttpClientForTests.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/java/OpenAiHttpClientForTests.java new file mode 100644 index 00000000000..38e7cb3861f --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/java/OpenAiHttpClientForTests.java @@ -0,0 +1,104 @@ +import com.openai.core.RequestOptions; +import com.openai.core.http.Headers; +import com.openai.core.http.HttpClient; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.NotNull; + +// Wraps httpClient calls to dump request/responses records to be used with the mocked backend +public class OpenAiHttpClientForTests implements HttpClient { + private final Path recordsDir; + private final HttpClient delegate; + + // Intercepts and dumps a request/response to a record file + public OpenAiHttpClientForTests(HttpClient delegate, Path recordsDir) { + this.recordsDir = recordsDir; + this.delegate = delegate; + } + + @NotNull + @Override + public HttpResponse execute( + @NotNull HttpRequest request, @NotNull RequestOptions requestOptions) { + HttpResponse response = delegate.execute(request, requestOptions); + return wrapIfNeeded(request, response); + } + + @NotNull + @Override + public CompletableFuture executeAsync( + @NotNull HttpRequest request, @NotNull RequestOptions requestOptions) { + return delegate + .executeAsync(request, requestOptions) + .thenApply(response -> wrapIfNeeded(request, response)); + } + + @Override + public void close() { + delegate.close(); + } + + private HttpResponse wrapIfNeeded(HttpRequest request, HttpResponse response) { + if (RequestResponseRecord.exists(recordsDir, request)) { + // will NOT rewrite the record file if it exists + return response; + } + return new ResponseRequestInterceptor(request, response, recordsDir); + } + + private static class ResponseRequestInterceptor implements HttpResponse { + private final HttpRequest request; + private final HttpResponse response; + private final Path recordsDir; + private final ByteArrayOutputStream responseBody; + + private ResponseRequestInterceptor( + HttpRequest request, HttpResponse response, Path recordsDir) { + this.request = request; + this.response = response; + this.recordsDir = recordsDir; + responseBody = new ByteArrayOutputStream(); + } + + @Override + public int statusCode() { + return response.statusCode(); + } + + @NotNull + @Override + public Headers headers() { + return response.headers(); + } + + @NotNull + @Override + public InputStream body() { + InputStream body = response.body(); + return new InputStream() { + @Override + public int read() throws IOException { + int b = body.read(); + // capture body while it's consumed + responseBody.write(b); + return b; + } + }; + } + + @Override + public void close() { + try { + RequestResponseRecord.dump(recordsDir, request, response, responseBody.toByteArray()); + } catch (IOException e) { + throw new RuntimeException(e); + } + response.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/java/RequestResponseRecord.java b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/java/RequestResponseRecord.java new file mode 100644 index 00000000000..3466f682d4a --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/java/RequestResponseRecord.java @@ -0,0 +1,192 @@ +import com.openai.core.http.Headers; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpRequestBody; +import com.openai.core.http.HttpResponse; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.MessageDigest; +import java.util.List; +import java.util.Map; + +public class RequestResponseRecord { + /** + * Turn it on when the tests change to identify which records have been used and which can be + * removed. This sets the execution attribute of the record file, so Git recognizes the file as + * changed. This is useful for identifying unused records when changing tests. + */ + public static final boolean SET_RECORD_FILE_ATTR_ON_READ = false; + + private static final String RECORD_FILE_HASH_ALG = "MD5"; + private static final String METHOD = "method: "; + private static final String PATH = "path: "; + private static final String BEGIN_REQUEST_BODY = "-- begin request body --"; + private static final String END_REQUEST_BODY = "-- end request body -- "; + private static final String STATUS_CODE = "status code: "; + private static final String BEGIN_RESPONSE_HEADERS = "-- begin response headers --"; + private static final String END_RESPONSE_HEADERS = "-- end response headers --"; + private static final String BEGIN_RESPONSE_BODY = "-- begin response body --"; + private static final String END_RESPONSE_BODY = "-- end response body --"; + private static final String KEY_VALUE_SEP = ": "; + private static final char LINE_SEP = '\n'; + + public final int status; + public final Map headers; + public final byte[] body; + + private RequestResponseRecord(int status, Map headers, byte[] body) { + this.status = status; + this.headers = headers; + this.body = body; + } + + public static String requestToFileName(String method, byte[] requestBody) { + try { + MessageDigest digest = MessageDigest.getInstance(RECORD_FILE_HASH_ALG); + byte[] bytes = digest.digest(requestBody); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + // split hash to two haves, so it doesn't trigger the scanner on the commit + String hash = sb.toString(); + int mid = hash.length() / 2; + String firstHalf = hash.substring(0, mid); + String secondHalf = hash.substring(mid); + return firstHalf + '+' + secondHalf + '.' + method + ".rec"; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean exists(Path recordsDir, HttpRequest request) { + String filename = + requestToFileName(request.method().toString(), readRequestBody(request).toByteArray()); + Path targetDir = recordSubpath(recordsDir, request); + Path filePath = targetDir.resolve(filename); + return filePath.toFile().exists(); + } + + private static ByteArrayOutputStream readRequestBody(HttpRequest request) { + ByteArrayOutputStream requestBodyBytes = new ByteArrayOutputStream(); + try (HttpRequestBody requestBody = request.body()) { + if (requestBody != null) { + requestBody.writeTo(requestBodyBytes); + } + } + return requestBodyBytes; + } + + private static Path recordSubpath(Path recordsDir, HttpRequest request) { + Path result = recordsDir; + for (String segment : request.pathSegments()) { + result = result.resolve(segment); + } + return result; + } + + public static void dump( + Path recordsDir, HttpRequest request, HttpResponse response, byte[] responseBody) + throws IOException { + ByteArrayOutputStream requestBodyBytes = readRequestBody(request); + Path targetDir = recordSubpath(recordsDir, request); + Files.createDirectories(targetDir); + String filename = + requestToFileName(request.method().toString(), requestBodyBytes.toByteArray()); + Path filePath = targetDir.resolve(filename); + + try (BufferedWriter out = Files.newBufferedWriter(filePath.toFile().toPath())) { + out.write(METHOD); + out.write(request.method().toString()); + out.write(LINE_SEP); + + out.write(PATH); + String path = String.join("/", request.pathSegments()); + out.write(path); + out.write(LINE_SEP); + + out.write(BEGIN_REQUEST_BODY); + out.write(LINE_SEP); + out.write(new String(requestBodyBytes.toByteArray(), StandardCharsets.UTF_8)); + out.write(LINE_SEP); + out.write(END_REQUEST_BODY); + out.write(LINE_SEP); + + out.write(STATUS_CODE); + out.write(Integer.toString(response.statusCode())); + out.write(LINE_SEP); + + out.write(BEGIN_RESPONSE_HEADERS); + out.write(LINE_SEP); + Headers headers = response.headers(); + for (String name : headers.names()) { + List values = headers.values(name); + if (values.size() == 1) { + out.write(name); + out.write(KEY_VALUE_SEP); + out.write(values.get(0)); + out.write(LINE_SEP); + } + } + out.write(END_RESPONSE_HEADERS); + out.write(LINE_SEP); + + out.write(BEGIN_RESPONSE_BODY); + out.write(LINE_SEP); + out.write(new String(responseBody)); + out.write(LINE_SEP); + out.write(END_RESPONSE_BODY); + out.write(LINE_SEP); + } + } + + public static RequestResponseRecord read(Path recFilePath) { + int statusCode = 200; + Map headers = new java.util.HashMap<>(); + StringBuilder bodyBuilder = new StringBuilder(); + + try { + List lines = Files.readAllLines(recFilePath, StandardCharsets.UTF_8); + + boolean inResponseHeaders = false; + boolean inResponseBody = false; + + for (String line : lines) { + if (line.startsWith(STATUS_CODE)) { + statusCode = Integer.parseInt(line.substring(STATUS_CODE.length())); + } else if (line.equals(BEGIN_RESPONSE_HEADERS)) { + inResponseHeaders = true; + } else if (line.equals(END_RESPONSE_HEADERS)) { + inResponseHeaders = false; + } else if (inResponseHeaders && line.contains(KEY_VALUE_SEP)) { + int arrowIndex = line.indexOf(KEY_VALUE_SEP); + String name = line.substring(0, arrowIndex); + String value = line.substring(arrowIndex + KEY_VALUE_SEP.length()); + headers.put(name, value); + } else if (line.equals(BEGIN_RESPONSE_BODY)) { + inResponseBody = true; + } else if (line.equals(END_RESPONSE_BODY)) { + inResponseBody = false; + } else if (inResponseBody) { + if (bodyBuilder.length() > 0) { + bodyBuilder.append(LINE_SEP); + } + bodyBuilder.append(line); + } + } + + if (SET_RECORD_FILE_ATTR_ON_READ) { + Files.setPosixFilePermissions(recFilePath, PosixFilePermissions.fromString("rwxr-xr-x")); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + byte[] body = bodyBuilder.toString().getBytes(StandardCharsets.UTF_8); + return new RequestResponseRecord(statusCode, headers, body); + } +} diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/44bf60144d870dad+6b2cb462d6bbc6a8.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/44bf60144d870dad+6b2cb462d6bbc6a8.POST.rec new file mode 100644 index 00000000000..68e7f13f7b8 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/44bf60144d870dad+6b2cb462d6bbc6a8.POST.rec @@ -0,0 +1,57 @@ +method: POST +path: chat/completions +-- begin request body -- +{"messages":[{"content":"","role":"system"},{"content":"","role":"user"}],"model":"gpt-4o-mini","stream":true} +-- end request body -- +status code: 200 +-- begin response headers -- +access-control-expose-headers: X-Request-ID +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 99e16aee9dc7757b-SEA +content-type: text/event-stream; charset=utf-8 +date: Thu, 13 Nov 2025 21:38:43 GMT +openai-organization: datadog-staging +openai-processing-ms: 151 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 164 +x-openai-proxy-wasm: v0.1 +x-ratelimit-limit-requests: 30000 +x-ratelimit-limit-tokens: 150000000 +x-ratelimit-remaining-requests: 29999 +x-ratelimit-remaining-tokens: 149999997 +x-ratelimit-reset-requests: 2ms +x-ratelimit-reset-tokens: 0s +x-request-id: req_42945a0327a0431888a554256fb3b910 +-- end response headers -- +-- begin response body -- +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"obfuscation":"dgaNTu"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}],"obfuscation":"u2k"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"!"},"logprobs":null,"finish_reason":null}],"obfuscation":"EUjiJiQ"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"obfuscation":"6N1H"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"obfuscation":"zIEq"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"obfuscation":"HT9MVR"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"obfuscation":"v"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"obfuscation":"FTR4"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":" today"},"logprobs":null,"finish_reason":null}],"obfuscation":"RK"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"obfuscation":"AJCTfFM"} + +data: {"id":"chatcmpl-CbZJbEp1qLmv74ugdNPjZqxHad0nu","object":"chat.completion.chunk","created":1763069923,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"obfuscation":"tF"} + +data: [DONE] + +�� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/4f35907d5d3bd522+45c20bf0159cc485.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/4f35907d5d3bd522+45c20bf0159cc485.POST.rec new file mode 100644 index 00000000000..7a82f37892a --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/4f35907d5d3bd522+45c20bf0159cc485.POST.rec @@ -0,0 +1,79 @@ +method: POST +path: chat/completions +-- begin request body -- +{"messages":[{"content":"David Nguyen is a sophomore majoring in computer science at Stanford University and has a GPA of 3.8.\nDavid is an active member of the university's Chess Club and the South Asian Student Association.\nHe hopes to pursue a career in software engineering after graduating.","role":"user"}],"model":"gpt-4o-mini","tools":[{"function":{"name":"extract_student_info","description":"Get the student information from the body of the input text","parameters":{"type":"object","properties":{"name":{"type":"string","description":"Name of the person"},"major":{"type":"string","description":"Major subject."},"school":{"type":"string","description":"The university name."},"grades":{"type":"integer","description":"GPA of the student."},"clubs":{"type":"array","description":"School clubs for extracurricular activities. ","items":{"type":"string","description":"Name of School Club"}}}}},"type":"function"}]} +-- end request body -- +status code: 200 +-- begin response headers -- +access-control-expose-headers: X-Request-ID +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 9a239477d81b30b7-SEA +content-type: application/json +date: Fri, 21 Nov 2025 22:21:26 GMT +openai-organization: datadog-staging +openai-processing-ms: 860 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 878 +x-openai-proxy-wasm: v0.1 +x-ratelimit-limit-requests: 30000 +x-ratelimit-limit-tokens: 150000000 +x-ratelimit-remaining-requests: 29999 +x-ratelimit-remaining-tokens: 149999930 +x-ratelimit-reset-requests: 2ms +x-ratelimit-reset-tokens: 0s +x-request-id: req_b8e819d6978e4c74ab692e06904acd49 +-- end response headers -- +-- begin response body -- +{ + "id": "chatcmpl-CeTnKb8ckpcNU5H2cbnhcYUbwTU4W", + "object": "chat.completion", + "created": 1763763686, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_LWUWpxL4zvZ6MlyuxN5xD529", + "type": "function", + "function": { + "name": "extract_student_info", + "arguments": "{\"name\":\"David Nguyen\",\"major\":\"computer science\",\"school\":\"Stanford University\",\"grades\":3.8,\"clubs\":[\"Chess Club\",\"South Asian Student Association\"]}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 152, + "completion_tokens": 44, + "total_tokens": 196, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_560af6e559" +} +� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/62a1fc1ad4af5c72+97c9474801aed42b.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/62a1fc1ad4af5c72+97c9474801aed42b.POST.rec new file mode 100644 index 00000000000..6a276694e1e --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/62a1fc1ad4af5c72+97c9474801aed42b.POST.rec @@ -0,0 +1,69 @@ +method: POST +path: chat/completions +-- begin request body -- +{"messages":[{"content":"","role":"system"},{"content":"","role":"user"}],"model":"gpt-4o-mini"} +-- end request body -- +status code: 200 +-- begin response headers -- +access-control-expose-headers: X-Request-ID +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 99e16aeafacd757b-SEA +content-type: application/json +date: Thu, 13 Nov 2025 21:38:43 GMT +openai-organization: datadog-staging +openai-processing-ms: 260 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 277 +x-openai-proxy-wasm: v0.1 +x-ratelimit-limit-requests: 30000 +x-ratelimit-limit-tokens: 150000000 +x-ratelimit-remaining-requests: 29999 +x-ratelimit-remaining-tokens: 149999995 +x-ratelimit-reset-requests: 2ms +x-ratelimit-reset-tokens: 0s +x-request-id: req_9a4c02351d94492d97183b76e5fb6220 +-- end response headers -- +-- begin response body -- +{ + "id": "chatcmpl-CbZJbm24JM36QwiDvRFMX7Nc4Gk0e", + "object": "chat.completion", + "created": 1763069923, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 11, + "completion_tokens": 9, + "total_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_51db84afab" +} +� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/d6d3881e8743ea24+154d4ae4e0a9a9b6.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/d6d3881e8743ea24+154d4ae4e0a9a9b6.POST.rec new file mode 100644 index 00000000000..9c4e2c1cfe7 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/chat/completions/d6d3881e8743ea24+154d4ae4e0a9a9b6.POST.rec @@ -0,0 +1,107 @@ +method: POST +path: chat/completions +-- begin request body -- +{"messages":[{"content":"David Nguyen is a sophomore majoring in computer science at Stanford University and has a GPA of 3.8.\nDavid is an active member of the university's Chess Club and the South Asian Student Association.\nHe hopes to pursue a career in software engineering after graduating.","role":"user"}],"model":"gpt-4o-mini","tools":[{"function":{"name":"extract_student_info","description":"Get the student information from the body of the input text","parameters":{"type":"object","properties":{"name":{"type":"string","description":"Name of the person"},"major":{"type":"string","description":"Major subject."},"school":{"type":"string","description":"The university name."},"grades":{"type":"integer","description":"GPA of the student."},"clubs":{"type":"array","description":"School clubs for extracurricular activities. ","items":{"type":"string","description":"Name of School Club"}}}}},"type":"function"}],"stream":true} +-- end request body -- +status code: 200 +-- begin response headers -- +access-control-expose-headers: X-Request-ID +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 9ab0127bce03ba52-SEA +content-type: text/event-stream; charset=utf-8 +date: Mon, 08 Dec 2025 23:34:13 GMT +openai-organization: datadog-staging +openai-processing-ms: 410 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 426 +x-openai-proxy-wasm: v0.1 +x-ratelimit-limit-requests: 30000 +x-ratelimit-limit-tokens: 150000000 +x-ratelimit-remaining-requests: 29999 +x-ratelimit-remaining-tokens: 149999930 +x-ratelimit-reset-requests: 2ms +x-ratelimit-reset-tokens: 0s +x-request-id: req_a6022bce08cf4e559712815db1b38146 +-- end response headers -- +-- begin response body -- +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_faEduzCTGcPKWBfwOYYnFynN","type":"function","function":{"name":"extract_student_info","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"obfuscation":"Y9RWtxFO"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"8kWaobgcJCw"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"name"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"hwjlr7KiFi"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"7Ts7vVAYI"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"David"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"CSiOGw4ct"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Nguyen"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"J03ySTV"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"5H8Dc5xW1"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"major"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"7wTP7X6LW"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"U2Btm05PM"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"computer"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"lE9asW"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" science"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"4WDbkz"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"RhzzC5kzm"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"school"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"RieTv78w"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"iZAdfItkI"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Stan"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"4JqJB8BYMe"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ford"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"Lqg3nlFkO9"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" University"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"DbE"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"KLVWPhENI"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"grades"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"pKuiBmsD"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"IUz9S0R37Xj"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"3"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"PvIBG3qh61zZy"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"."}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"9aVqBdPmeFS0f"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"8"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"PAZXiQ7oPErvT"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":",\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"TuzQveZYkYq"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"clubs"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"W1z0bbQRQ"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":[\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"8QFmMgzA"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Chess"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"NAEI3uqPM"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Club"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"G6vQwokWp"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"3q3fWfLiT"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"South"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"NlR6ZSKVA"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Asian"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"KrajmqM3"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Student"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"847pHO"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Association"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"cK"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"]"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"Wb1xRWHMd80"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"}"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"YMh6zFFfobQpz"} + +data: {"id":"chatcmpl-Ckf25aF36HtqV5JYhu61IvIkl7mib","object":"chat.completion.chunk","created":1765236853,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_aa07c96156","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"obfuscation":"t9YalpVxtKhv"} + +data: [DONE] + +�� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/completions/1288497d87888ffe+d0ea0a99184a54b9.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/completions/1288497d87888ffe+d0ea0a99184a54b9.POST.rec new file mode 100644 index 00000000000..6e6830145d0 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/completions/1288497d87888ffe+d0ea0a99184a54b9.POST.rec @@ -0,0 +1,56 @@ +method: POST +path: completions +-- begin request body -- +{"model":"gpt-3.5-turbo-instruct","prompt":"Tell me a story about building the best SDK!"} +-- end request body -- +status code: 200 +-- begin response headers -- +access-control-allow-origin: * +access-control-expose-headers: X-Request-ID +alt-svc: h3=":443"; ma=86400 +cache-control: no-cache, must-revalidate +cf-cache-status: DYNAMIC +cf-ray: 99e16b2f5d0febc1-SEA +content-type: application/json +date: Thu, 13 Nov 2025 21:38:55 GMT +openai-model: gpt-3.5-turbo-instruct:20230824-v2 +openai-organization: datadog-staging +openai-processing-ms: 812 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +via: envoy-router-6f6c68f688-2phpl +x-content-type-options: nosniff +x-envoy-upstream-service-time: 847 +x-openai-proxy-wasm: v0.1 +x-ratelimit-limit-requests: 3500 +x-ratelimit-limit-tokens: 90000 +x-ratelimit-remaining-requests: 3498 +x-ratelimit-remaining-tokens: 89988 +x-ratelimit-reset-requests: 17ms +x-ratelimit-reset-tokens: 8ms +x-request-id: req_6756cf3bf5464dd6999ce0f4906efbf0 +-- end response headers -- +-- begin response body -- +{ + "id": "cmpl-CbZJmTHXjJZ6AvfMFVuo0xx6wIu33", + "object": "text_completion", + "created": 1763069934, + "model": "gpt-3.5-turbo-instruct:20230824-v2", + "choices": [ + { + "text": "\n\nOnce upon a time, in a small tech company, a team of developers", + "index": 0, + "logprobs": null, + "finish_reason": "length" + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 16, + "total_tokens": 26 + } +} +� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/completions/80e69870b51de0ae+65e91e5d059acb41.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/completions/80e69870b51de0ae+65e91e5d059acb41.POST.rec new file mode 100644 index 00000000000..cf1035ed5d7 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/completions/80e69870b51de0ae+65e91e5d059acb41.POST.rec @@ -0,0 +1,57 @@ +method: POST +path: completions +-- begin request body -- +{"model":"gpt-3.5-turbo-instruct","prompt":"Tell me a story about building the best SDK!","stream":true} +-- end request body -- +status code: 200 +-- begin response headers -- +access-control-allow-origin: * +access-control-expose-headers: X-Request-ID +alt-svc: h3=":443"; ma=86400 +cache-control: no-cache, must-revalidate +cf-cache-status: DYNAMIC +cf-ray: 99e16b367e7eebc1-SEA +content-type: text/event-stream +date: Thu, 13 Nov 2025 21:38:55 GMT +openai-model: gpt-3.5-turbo-instruct:20230824-v2 +openai-organization: datadog-staging +openai-processing-ms: 105 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +via: envoy-router-67cd4df8f9-284k6 +x-content-type-options: nosniff +x-envoy-upstream-service-time: 125 +x-openai-proxy-wasm: v0.1 +x-ratelimit-limit-requests: 3500 +x-ratelimit-limit-tokens: 90000 +x-ratelimit-remaining-requests: 3498 +x-ratelimit-remaining-tokens: 89988 +x-ratelimit-reset-requests: 17ms +x-ratelimit-reset-tokens: 8ms +x-request-id: req_79c9739603b146e0ad4d78742c92ba1e +-- end response headers -- +-- begin response body -- +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":"\n\n","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":"Once","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":" upon","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":" a","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":" time","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":" in","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":" a","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":" kingdom","index":0,"logprobs":null,"finish_reason":null}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: {"id":"cmpl-CbZJnedq10GrVAeKa8ANHpdf8IUtn","object":"text_completion","created":1763069935,"choices":[{"text":" far away, there was a group of","index":0,"logprobs":null,"finish_reason":"length"}],"model":"gpt-3.5-turbo-instruct:20230824-v2"} + +data: [DONE] + +�� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/embeddings/87e12d2bd4c95948+3727223adbe9f234.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/embeddings/87e12d2bd4c95948+3727223adbe9f234.POST.rec new file mode 100644 index 00000000000..945d94a3b2c --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/embeddings/87e12d2bd4c95948+3727223adbe9f234.POST.rec @@ -0,0 +1,51 @@ +method: POST +path: embeddings +-- begin request body -- +{"input":"hello world","model":"text-embedding-ada-002","encoding_format":"base64"} +-- end request body -- +status code: 200 +-- begin response headers -- +access-control-allow-origin: * +access-control-expose-headers: X-Request-ID +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 99e1e64fb806ded8-SEA +content-type: application/json +date: Thu, 13 Nov 2025 23:02:57 GMT +openai-model: text-embedding-ada-002-v2 +openai-organization: datadog-staging +openai-processing-ms: 129 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +via: envoy-router-5c77bdcc4-c7xd6 +x-content-type-options: nosniff +x-envoy-upstream-service-time: 156 +x-openai-proxy-wasm: v0.1 +x-ratelimit-limit-requests: 10000 +x-ratelimit-limit-tokens: 10000000 +x-ratelimit-remaining-requests: 9999 +x-ratelimit-remaining-tokens: 9999998 +x-ratelimit-reset-requests: 6ms +x-ratelimit-reset-tokens: 0s +x-request-id: req_588b37f827b244ccae42475ca15df118 +-- end response headers -- +-- begin response body -- +{ + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "GOODvIRlszptnp+8jQMKvUUK1bxHhfs7V+fLvMDipLlFClW8P62xvJL51jxD9gQ8Tw8Cvf1zZrunrJo7YpFSPDZukTysnte8ebOMPK5wFDwFp9a7ib7GO4bcyTu3pxS8zQ9yvIB/Jry8/Jc8oOCAvAd5kzwmcxe9kvnWOkQGRbsjkZq889F/vEjoQTwhIiS8DuLmOHRimbx9pck8q46XvNvyCzxQExK7lj6aPEKPrrxOpBu9JJm6O6XaXbmC+sy8bqrPuvh5CT29FPg8z+W+u5HxtrxwfAw8RPqUvM5yuDzVRvK8ILOtPMkl1TzwSqm8AsXZOkEs6DvGpp68XURvPKiwKjzcCuw7faE5PFEnYjvr/UW808MrO3hMtjyx92o8uBaLvLPFl7twgJw8KlUUu8qUy7w7v4S7NP8aO4hPUDunrBo9m48NvVP9Lryw56o7IR6UPPWbHDx3QAa8S87OPIbgWbxVZIW839yoOyEaBDs+Tvs7huDZOwr4Sbw46bc7XCwPvG8Nljwo5p056pr/vFhOorxn5lW8gZN2vGGFIrxtmo+8YBo8vPLJ3ztn5lU7C2OwPIm+xju/h368gH+mPO7TErxH2AG9x7beu3GQXLuiUwe88r0vu1OaaLw7y7S8EKwDPKi0ujwZ88M8hdCZvPrsDzzNZog7ySFFvbTNN7yOE8q7JQCRvFGGGD3al+U79DjWPNPLS7w7aO68mRyHPAiBs7wlAJE8ke2mvAYOrbyaIJe76wHWPBOOgDyC+sy7yBGFu0jsUbvcYYI8mzBXO34IkDqAexa8wOY0u8/dnjuWPpo7WE4iPMgZpTweOIc8MoyUuw7e1jyw67o7aysZvAWnVrmw6zo7/+bsO06gC70N0qY7mK0QPfxntjwndyc82By/O1KW2LziuhW8G2paPLPFF73XuXg8TqCLunrH3DvnDxk85wsJPdQqArzwUkm8WV7ivCSZujxCix48CZHzPAYGjbu0ySe7Pk77O+WckjqyWrE77Xjsu/WjPDxcLA89aE2sPA7iZjyHRzC/pj0kvGKNQjytBS485ZwSPIyYozzlnBI8oleXPL+H/rsknco8ZnvvulhGgjziuhU748pVvNBIBbwVBRe8dt0/PIXUqbzxuZ+8hG3TPFP1Drw1D9s8ZW+/vAHBybtT9Y667tOSOy5DwTrL84G8DuJmvP7WLD2F1Km8KOYdPCC3PTwmFOE6kY5wPWF9gjyLkIO8olcXPOFXTzx5uyw9BatmvC03kbw7w5Q71xAPvBC0IzupwGo8C1sQPINhI7wszKq7CvCpu/YGAz3gT6+7ddGPPB5AJzw5UA68DdImvJHxtjz6lfm7esdcu11EbzwqXTS7fgSAPIoprbpsN8k72CRfvKyeVzxn7vW84rqVO9WdiDuXRrq7UBsyO91tsjsp8s28DdpGvLyhcTw0+wo9C1eAPMeyTrxqwLK7jajjPNv2mzxRJ+I7ayuZvAWnVry8/Jc8sU6BvDYX+7zzLCa7J3/Hu40DijpvDRY9VWgVPCzImrxI8OG8/+bsPEAYmDu3s8S7qSOxO+xorDyG5Om7JhRhuzlUHjyg4AC8T7z7O+cPGT3b8ou7es98u8Du1DwOPY08sVKRvHfp7zs6WK66TNbuvNTTazwYgL07cCX2vPqV+ToUmrA8KfLNPLkaG7yDWQM9WPN7vIP+3DwzmMQ6s8GHulGGmDzaj8W7geoMvLPFF7wTkpC8Nm6RPB9Q57sSyHM80mBlvP9JMzzM/zG8XDSvu3lcdrzOelg7znrYO4NZA7yDYaO7lMcDvJepgDql4n08qivRvE+su7yyVqG79DRGOy0zAbk7v4Q79Z8svDjhF71I7NE8eiIDvJ5xirucn807SOzRvOe0cry8ofG8dXZpO9PDKzz7W4a84sZFPMuca7vq7QW9n326vIm2pjy2RM46AbUZvfxvVjsFq2Y7TTk1vHMDYzsePJe6EbzDPOPKVbzoG8m74baFO7i3VLxVZIU8uoUBvMPEobzhW1885aAiPLuVQTomb4e55ZgCPYiuhrydAhQ7mig3PLZEzjsoh2e8Uo44u4iuhjstNxG8wOIkPJosx7o9Mos8DG9gPOv9RT0bato7VoD1PGfixbx9qdm7K2XUvCd/x7vcZZK81CqCPNwGXDys/Y076vGVvE8Pgjtm0oU8n3UaPP3ODD28+Ie7r4RkuzEZjry13fc67GQcPAHByTvIHTW8aysZvPYGgzz3GtM82YMVPFP9Lj0mbwc8JQgxvMuc67zL9xE8qitRPG8RpjqmPSS7EKwDPH2dKTxymPy8isr2PM+C+LvTz9u7rnAUPIXMCT1Lzs68cveyu08PAj1Ch448TNbuu/MwNjsN0iY9/G9WvO1wTLop+m28GIztO8PMQTxkYw+9KfptO+PKVbuF0Bk9pkG0PJosxzwAWvM7+1uGOzT/mryKIQ268EqpvJXXQzxH3BG8CvxZurDrujipGxG8pNI9Oxr/czzb/ru8XqtFPGbShbtgHky4g/5cOz02Gzy/h347YYmyvG2mv7xKW0g7OVAOPKY5FDq9FPi8n3UavbNq8Tq6Lus6rJ7XO0eFe7zkNTw7PkbbPGVzz7w5UA48l07aO6onwTwnd6e7FQEHu2w3SbwlqXo8qRuRugiBM7z0NMa8ELQjPFwwnzlRhhi8ofDAvNqXZbo0+wo7gva8vKNfNzrU02u82/47vHbVHzz1ozw6fggQvFwwHzwAWvM8NmoBvFQN77tla6+8kvXGvEUSdbyjY0c94VM/PGm4krtfEpw7qLjKu8kp5bvkNTy9aFG8vBInqjsk/AC8iE9QO34IELw3flG79gqTPAr82TxSjrg7fgSAPM/lvrwVCSe8GloaPMgdNbvai7W7PqWRu4NZgzzHqq48R4X7upRs3TsQVW08faG5u80P8rtI7FE7TUFVO44LqjxPsEs8PqGBPBUBBz2i/HA8UBuyPMQvCDy3p5S86Y7PO3CAHDwgr527+HkJvGKNQjzP5T48GIA9vH6taTzXuXg7KlUUvdK7Cz2LNV25M5zUuxjjg7y6hYG8EKyDPCCzrbxpWVy8YBo8vMPAkbu9a465fyBwvNVG8jtQH0K6r4TkvGlVzLwp+u28pkG0POxknLxtmg87ZntvvAxnwLw9Mou8MRkOuxSasDz0lww8nat9vIbkaTudCjQ8lGxdPHLzIrxW1wu9miCXOk1BVbuw3wo8Pk77O/89AzwlDME7Bg4tvMwH0jxuqk880VAlPEjsUTzHtl68tkjePFvNWDpkBNk7sl7BuxUBB7xy8yI7xqaeurZAPrwXeB28o2fXuUEkSDslBCE8bqrPPMVD2DyF2Lm8CZFzvJs49zyco9282By/PNl/BbyVzyM4mizHPHL3MjzM+yE9rJ7XOh+rjbxMLQW8NmoBvV6v1bqF1Kk8HMUAPTOYxDux89o7zWqYvFhGgrvHrj68GIxtvE+0Wzz69K+7n3WavAh9o7z8b1a8SmNovJybvTsYjG27pjmUvHRmqbxNOTW8mcXwOxd4nbuYsSC97tcivaNfN7yS+VY5aEUMvAzKBjzTwyu6ibo2uurthbwccnq8q5rHO6HsMLwhIqQ7eFRWvJHlBj1RI1I8Eh8KPfMkBjxJUyg8XTxPPNwK7LueFmS8ljqKu9qPRTr/PYO8Bx5tPLkaGz0WGec8Kfrtu0vOTjyZxXC7BaPGuh9Q57stNxE8ySHFvKDkkLz+2ry8Dt5WOxtiOrsfUOe6jahjPB9M1zv2BoM8lkKqPI92kDwTjoC7Pk77POQ1vLxwfIw8n4HKusqUSzyFzIm7eyYTu2rAsrv2r2w6N4JhPPxrxjz1m5w8OVAOvGvMYrzkNTy8Z+bVPJepALu4u+S6HMmQOyCvnbxY69s7ezLDvLNq8TsK9Dm8WE6iO9PHu7sgt728yYSLPDn1Z7ySAfe8uBaLu0fksTvdbTI95aCiO/Q8ZjzVPlI8CInTO9FQJby9FPg74PB4vA9Frbsp+u0817l4PDZukbtQGzK8DGtQPPMkBjrB8mS8l1LqvCZvBzwN2sY8t1D+u0Z1u7w2djE8VXA1vVvJyDxYRoK7WsU4vCONirznEym7hGnDvCpVFDytAR697teiPEwxlTxOoIu8FJYgvEVtG7hm1pW8l0pKO0Z5S7wJkXM8XqOlvPav7DuLLb08z9kOPJIB97wWGWe6QSzoO+/rcjyovNq8o1+3u/YGA7xH5DE8l0pKPPxv1jvSuwu8isr2u4CDNrxUBc+7HMmQPIoprTvM+yE6vRR4vKU1BLzv49K8O8ckve/rcjvRUCU7MoyUvOJjf7zhW1+8p0lUuzvHpDxMLQU8zQ9yvOMtHDxPDwI9Qo+uvNgk37tm3rW7D0k9PPsA4LxX4zs8T7DLu9qT1bwFq+Y6O8OUvPxjJrzf3Ki6d+lvPGw7WTy6Lms8bD9puuVB7DzRUKU8s8WXO52r/bu9FHi8r4TkPM/ZjrwTO/q657Tyu+7Tkrv1nyw7hdCZu1fbG7sccnq7d0AGvSvEirxPtFs7ldvTO5yfzbv/QRO7IR4UvZNYjbwfUOe8RAI1PGRnHzxQH8K8Tkl1PDT7CjtdPM85WVrSO/Mwtrs+Tvu7cwPjvOK6FbxAueG77+tyvJi5QLsQtKM8l1LqOkeFe7vZf4W89goTvI8X2rzdaaK8LkdRvJL1xjt5uyw8aWF8PEEs6LtT/a48ZATZO2hNLDyudCQ8w8ixvLH3ajzr+bW6QSxoPCWp+jtZWtK8dtkvuzu/hDwknUo8UBcivOv1pTzULpI7MRkOvO1wTLyw34q7RWmLu2LwCDwZ88M8k2AtvBd4nTuuEV45gHeGOnRimToa//O5rQk+PKXWTbzIFZU8lj6avMU3KLy6hQE7t6ukvGF9Aj1rL6m8JhThO6esGj14UEa6dGo5u81qmLrgRw+8JJ1KvCZvhzsTO3q7OOWnvEVpi7zL95G77tMSvG8NFr1y64I72BivvEAcqLs5VB47Tkn1PPQ8ZjzhtgW91DYyvGF9grzkOUw8Dc4WO1wwH7z/RSM8JQCRu7i7ZDpm1pU8BhI9vCZzF7w+Rts73AZcPGhNLDscxQA9x7ZePiUEobzNZgi8oOAAPai0OjwlAJE80mBlOzEZDjvLnOs5gva8POmCn7xY8/s7eEy2vIiuhjurmkc7fJUJu7wAqLyx78q8v94UveFb37yeFuQ6z+GuvB+rDb09Oqu8UBuyPDC617uspne8AzjguwnsGT1KX1g8gwZ9uzZqgbxoUTw89xIzPLJWobxrzOK7dW7JPKRv97uosCo9I5UqPKkfIbx5s4y8JhRhu4LunLu8ofG7NQtLPA5BHTwtMwG9dW7JPJbf4zxer1W8RnErPH2lyTzZLP88aWH8uS0zgbsZ5xM8dGIZPKY5lLsfq406slqxuyEiJD26Lus6KlEEPWm4krwVpmA7FJagvKkfobsAWvM7ay+pvLerJLzXsVi72CTfO+gn+bu4t9S8Ke69vJ91mjx0Yhk8dW7JPK5sBD0AsYk7nf4DvKyeVzxKW8i82/YbvbPBB70TM1q8OfFXPFfbm7syiIS88bWPuyYQ0bxYSpK6SVe4ufv8zzxI8OE8UYaYO3yViTwlCLG8ovxwPOmGr7yC+ky88zC2Ou/r8rr0NMY6miCXPPWfLLyaJCe7t6cUuyOVqrtLxi68R9iBvB5AJzykxo27DG9gu/MsJjyyWrG86YIfvNl/hTuqJ0G8XqOlOxnzQ7wmFOG7AE5Du8z7obvzKJa7iE9QvE5J9TtMMRU8qLAqvX8g8LrIGaW7sVKRPBIfijs7v4Q4jhPKu3VuSTwa//O6t7PEvEwthTsCzXk548pVvNgUH7xJTxg74ykMvHAl9rubkx09cHyMvGw/abxW1ws7Pk77vNgkX7x5txy7F3SNvHP/0jxFbZu8BavmvEeF+7zIEQU8xC+IumlhfLzxXnk8bgkGPdgcP7xT+R69W83YvEvGLr5Dk748BgYNPF6ntbxSjrg85xOpPMi+/jzUKoI74sI1u1EjUjuh6CC7pdpdvO3PAr2kyh28GeeTucuca7y4u2Q8zPuhPJokJz2cn8087XjsPI0DiryWQqo8K230uwxv4DsjNnS8jaTTOy5L4TyDBv06TDUlvM/dnrzUKoK87GScPD5CyzvGS3g8Eh8KvG8Zxjt6IgO9gHsWu4GL1jxzA+M8QShYPIB3Bjwd0bC8asAyvHMD4ztX25s8z+W+O5R0fTyV29M74mN/PEvGLrzvRpm7vm+eO3dIpjxI8GE8wl3LO44TyjzcYQI8YSp8vM/hrrtOqKu8Zt61OHB8DLw823Q6znpYuodHMLw7xyQ8+H2ZvFQFTzyaJKe7vRT4vPG5H7wJ6Am9duHPPCtt9DtDn+68iy29PE+sO7yl4n27eiKDu18WrDz68J86vAAoPKZBNLy9aw48pj2ku5ogFzx6x9y8y5xrvNQukjzGS3i8YYUiugIkkLyW3+M66/k1PJdKyjuXqYA8abSCvCzQujq/2oS77+vyO4LunLv4eQk8j3IAPWAaPLtGfds8C1+gPLFSkTx5XPa8x7LOvCn6bTymPaQ8cYSsPBCsgzxOSfU85wsJvLqJkbwTkhA7HjgHvEfYAT2FzIm5/c4MvbuRMTuD/lw7+1+WOxjjg71wfAy9YX0CPPQ4VjxhhSK8ovzwPOPOZbtQE5I7rKb3udQyojwATkM8WV7ivBC0o7zxuR+8CH2jPPLBv7xDl068bD/putQ2srzqkt884PD4u0EsaLzGph48ILtNu8Du1LzDxKE7eEw2vASXFj24u2Q8eEw2OYrKdryVz6O76BvJu26ybzrzJAa6FKJQO60FLr0rbfQ86Yo/PLNq8bwgrx091DYyPEZ1u7uYtTC9Xqe1ulOaaLrVPtI6yBklPSUIsTuaIJe8CeyZvKnA6rzcAsy8Ezt6O7evtDz0PGa7SU+YPJNcHT1Ch468bD/pOxSWoDwRuDM69xKzvCd/RzzVRvK76YIfN/G1j7ziviU8SOjBPD+xQTwyiIS8EsjzPI96ILzYFJ87huTpvOrxFTvP3Z67EiOavJRsXTwgr528PTYbvVq5iLwLBPq7ALGJu6oz8TxBLGg8kY5wu9VG8jsmc5e7rhn+vIoprbyXTto8bDM5PDT7CrwuPzE8EKyDOxOOALwuR9E77XhsPKkbkTyWOoq89adMvDu/hL3B8uQ8z+U+vOBPL7yubIQ8YpViO8562DtX25u8nJetuzUHO7wyiAS9Xqc1u40DCrx5XPY68bUPvW6y77vsBeY8hHFjPGP4qDxPD4I8oeggPPqV+byAfya8NmoBPIdDoLxpYXw87XDMvEfYgTsfq427wl1LvBSeQLr/QRO9XqtFupbf4zwHHu07pTUEtyC7zTpNObU8kemWO9598jx0Yhm9rQGevN/oWDzE2PG6Bx5tO0EguDsfqw28ng5Euy+upzwHdQM8wOa0OJdO2jzvRpm8K2XUvFAXorwHeZO8KlmkuyUAkbp5XHY6p6gKvRIrujxdnxW8uR6rPNatyLptnp86y/eRuwd5k7yx78q7jxfau5ZCqry5Iju8J3s3vLuZ0bqV21M8nJ/NPGKVYjzQ9X68AzDAPHLvEr3Odsg8kgF3PC3gejx4TLa8fxRAPK5sBD2ucJQ88ySGuvBOubsyKU67XDg/PJGO8Lz5iUm8IcPtOu7bsjwo4g28rhHeO65shDyjY0c8T7z7PB9IRzyyYlE8jgcaPOqSX7z7AGC8zAdSvD02G7t6x9y7Ezv6vPMkhrtsO1k8vPiHO8qImzxaxTg88slfPIc/kLx9qVm7nQakPFAfQrwvrie9/0ETPUlLCDuHR7A6znbIPKuOF7xH4KE8UoqoO+DweDoaVoo7Kl20PKHwQLs7aG48DMoGvM/ZDrwng9e8oIVavCn67Ts7aG47s2rxPJ0GJLyUy5M9c//Su3MD4zuUaE08P62xPCd/xzwSI5o86H6PPIB7Frwfqw29nnEKu3P7Qjwkodq61NPrvFVolbwQrAO89xrTvL/aBDoZ7zO8uo0hPMz7ITxptAI84sZFPPLFzzqDXZO8mii3vNqLNTxgHkw81DKivH4MIL07x6Q7Q5dOOwM00LmvhGS8n3mqO0T+JDyD/ty74Vvfu0eFezxm3jU8QouePCEipDtm3rW8hcwJvVKWWDzA7tQ6z4L4vNxlEruveDS8" + } + ], + "model": "text-embedding-ada-002-v2", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2 + } +} +� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/1f86220a2a41110e+2b22e08e8d61835a.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/1f86220a2a41110e+2b22e08e8d61835a.POST.rec new file mode 100644 index 00000000000..aa76244151b --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/1f86220a2a41110e+2b22e08e8d61835a.POST.rec @@ -0,0 +1,151 @@ +method: POST +path: responses +-- begin request body -- +{"input":[{"role":"user","content":"What's the weather like in San Francisco?"},{"type":"function_call","call_id":"call_123","name":"get_weather","arguments":"{\"location\": \"San Francisco, CA\"}"},{"type":"function_call_output","call_id":"call_123","output":"{\"temperature\": \"72°F\", \"conditions\": \"sunny\", \"humidity\": \"65%\"}"}],"model":"gpt-4.1","temperature":0.1,"stream":true} +-- end request body -- +status code: 200 +-- begin response headers -- +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 9af17879ae6f27a1-SEA +content-type: text/event-stream; charset=utf-8 +date: Tue, 16 Dec 2025 22:03:25 GMT +openai-organization: datadog-staging +openai-processing-ms: 40 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 43 +x-request-id: req_f9c12325105b41649d454608dc18a0c9 +-- end response headers -- +-- begin response body -- +event: response.created +data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_00cc8ab3b2084dff016941d72d1e9c8190a5e511d130ff2909","object":"response","created_at":1765922605,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":0.1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_00cc8ab3b2084dff016941d72d1e9c8190a5e511d130ff2909","object":"response","created_at":1765922605,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":0.1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","type":"message","status":"in_progress","content":[],"role":"assistant"}} + +event: response.content_part.added +data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"jo97isUEsZsqp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" weather","logprobs":[],"obfuscation":"qVAdC3Ar"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"CQ6o4QdFsf2jx"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" San","logprobs":[],"obfuscation":"wStaqwIFxCXd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" Francisco","logprobs":[],"obfuscation":"r5DvhR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"JkwphkuU9MphL"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" currently","logprobs":[],"obfuscation":"hIFsIb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" sunny","logprobs":[],"obfuscation":"Wp1tzjudGk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"CzEt2tfaOpU"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"I14aUoL0oZ3ZtE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" temperature","logprobs":[],"obfuscation":"HLUP"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"YTRH5VQlP0rGf"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"uYLsBsQHYDXli5n"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":"72","logprobs":[],"obfuscation":"CvQDLdAriunveN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":"°F","logprobs":[],"obfuscation":"4rCSpvAdxgRrNB"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"h0qvxQhOzHkn"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" humidity","logprobs":[],"obfuscation":"2cTzgJY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" at","logprobs":[],"obfuscation":"gSOyfzce0m9j5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"Seba65QERawFtyb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":"65","logprobs":[],"obfuscation":"8ZHpHAvhYnpbxV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":"%.","logprobs":[],"obfuscation":"TpBvfP0fZuqNcl"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" Let","logprobs":[],"obfuscation":"9ZcNuiWXRfE3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" me","logprobs":[],"obfuscation":"PCqUWwG9qmQxh"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" know","logprobs":[],"obfuscation":"kJ40n0bgL9j"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" if","logprobs":[],"obfuscation":"sqofzBiwpjD6M"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" you","logprobs":[],"obfuscation":"dv6tTu2BA94l"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" need","logprobs":[],"obfuscation":"e8SRktIOI47"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"H3IGcwIXZBUVn0"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" forecast","logprobs":[],"obfuscation":"H3SRX7V"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"dN0Psll46FHo"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"UDFJ9gAS9cbN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" coming","logprobs":[],"obfuscation":"jSFA5NZl9"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":" days","logprobs":[],"obfuscation":"QR6M2SAn7U4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"delta":"!","logprobs":[],"obfuscation":"BGTKPNvP7dO5rK2"} + +event: response.output_text.done +data: {"type":"response.output_text.done","sequence_number":38,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"text":"The weather in San Francisco is currently sunny with a temperature of 72°F and humidity at 65%. Let me know if you need a forecast for the coming days!","logprobs":[]} + +event: response.content_part.done +data: {"type":"response.content_part.done","sequence_number":39,"item_id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The weather in San Francisco is currently sunny with a temperature of 72°F and humidity at 65%. Let me know if you need a forecast for the coming days!"}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":40,"output_index":0,"item":{"id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The weather in San Francisco is currently sunny with a temperature of 72°F and humidity at 65%. Let me know if you need a forecast for the coming days!"}],"role":"assistant"}} + +event: response.completed +data: {"type":"response.completed","sequence_number":41,"response":{"id":"resp_00cc8ab3b2084dff016941d72d1e9c8190a5e511d130ff2909","object":"response","created_at":1765922605,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"msg_00cc8ab3b2084dff016941d72d5d908190944b51c77cc1e89b","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The weather in San Francisco is currently sunny with a temperature of 72°F and humidity at 65%. Let me know if you need a forecast for the coming days!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":0.1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":60,"input_tokens_details":{"cached_tokens":0},"output_tokens":35,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":95},"user":null,"metadata":{}}} + +�� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/cacab2d655cda645+7cb661ee9f414322.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/cacab2d655cda645+7cb661ee9f414322.POST.rec new file mode 100644 index 00000000000..f1e4706144a --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/cacab2d655cda645+7cb661ee9f414322.POST.rec @@ -0,0 +1,136 @@ +method: POST +path: responses +-- begin request body -- +{"include":["reasoning.encrypted_content"],"input":"If one plus a number is 10, what is the number?","model":"o4-mini","reasoning":{"effort":"medium","summary":"detailed"},"stream":true} +-- end request body -- +status code: 200 +-- begin response headers -- +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 9acf06c1a8d027da-SEA +content-type: text/event-stream; charset=utf-8 +date: Fri, 12 Dec 2025 17:43:51 GMT +openai-organization: datadog-staging +openai-processing-ms: 71 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 79 +x-request-id: req_f0f33936d66a44bebf5f27edb2ba4f18 +-- end response headers -- +-- begin response body -- +event: response.created +data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_00c6bd4b21da44ff01693c54579ff881968727cf568e228a75","object":"response","created_at":1765561431,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"o4-mini-2025-04-16","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_00c6bd4b21da44ff01693c54579ff881968727cf568e228a75","object":"response","created_at":1765561431,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"o4-mini-2025-04-16","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"rs_00c6bd4b21da44ff01693c5457f090819695b362fd1662a53e","type":"reasoning","encrypted_content":"gAAAAABpPFRX33FOspfsXtp3XbwX8YHQu2N8sbIHQwXkws1ydr9FzSyPawU3CpGcNJGAyuwSj4YpfM79bTfVW9SeqcQNmBeY01peKCJ6h2oPl2FeJmStCIOtZPyfvqYtQR4z1pNHAsDBKuEs_iH2-s-ZbGjCfLRtcqLuUe_OOa_BKMREj3Cpr-FLMAbHJYxwK0-Xm2jJ3pU-lXbXnV5l-Chc1UF38UApFqlPWDo5S5QWXFkN3dh4IA3-B8qyXpzqa37Tu9SIzcW-jr8n1UohouXp-3EvS1z7uUCAVMek565qb-YMU-wXRQUtwa3q7SeJlPo6tsRKPlSoKbdNYgzP5_A_JL5hPUq8FO1nA5fK57yPFg3azuaQCZB2CtIQxP0fWztO00DYGKpzJS2hmjWNKjoGAxIPySFKZGVwHnYRgZF9XDejHsgbTa-xz9Fn7nh0JUgf86hyeEj1z3hZPj5i9Hr7lPGZoAnUwoijz7hsl_sDSnrgIT_iW6Li3Jc2_Ckha7kFv554L-TIVGdadUNMRkznjL2WcxbSq5XIXcHMiIg-XIhNfe6IL5C6iUJR5dFrKuilePWj5yXrBznAPWTvmJJ_VPD-EqphwDdnApmYn0hFdBrbQS2vrlo6C-kMJxGtg_9NyRSwpQv48Bv1WqzMCJ7jyytl1FQSOpTA-DJ1M4VxuMnwMht_2hxZeZBwNfNQYUDjBJ63uojwZiokXj8sNgsZeI2vFA7MCVsEUSduBBwr72olXldZmGPsLb8ECHi9AaR7A7Bs28Pf9d9RbmrD7PC7V4rfSac2tk_vAmnHylvUuzoGtPaW8DjcmQUalXnVLcQNamr4j9Mj","summary":[]}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":3,"output_index":0,"item":{"id":"rs_00c6bd4b21da44ff01693c5457f090819695b362fd1662a53e","type":"reasoning","encrypted_content":"gAAAAABpPFRZT3BahyZprk37tTcEsFXo7GbflYdi9LIbXZrR7zyw0FYB3g8qWnBLVy5s8qc21Am0Jf5JZHYyEskrINfYnqmcosRbE3aMwbGRLoJ4QgV61_U0yPJwXi9ZR1Sce4KZjXFNs74l26KKxJeB92Q_mDZcHIKIJ3XPo6eRyWJlODzJYWFPEBihzcSmDOAwo_7lphaI9x2mpHBy16V33sxAgX1BzU8fAfmXdAyJxeRNJpuv9AExUpNAvt7ebaOUZj6_zILgF6OJI0UdBBm8I2TioKwuCSQGzFDuYfp03QvFb2Wixy_x4a5KlxvMbSs54sx89vseOBH0ga0ZoPJcJwFzu9MXxw8p-mNz6OrhEyrusFWYBxHn1P5FQKHFFxOb9hN8RsRB1YL2V78zTRhWF9P5oLzXuiRTGRRwKsHmIk_O9g6i9n0ZGZVobPN6Uwe_GLXQaN46toh83INZgniHT6vW67uODY7lixybAhuBF76cCRW7q8Re2HFvSRDehWjTE-RkhrLx1JUk5fApA4ulzzzaNv6wVveYWyolfXs1tbA1ARmWKRaTDH5wvTQsaxXdVyyv8sAnaGPbVjWF6bi3pHOiQJyoAdZjIpBXH91uOd0788WidOUCSmEAguBcN9hHEddW23GBnUkFPl2EuDtdsLGfywN7fXcQZ5I02h-vDP-mjfhuHYabx5GgQztBbfg0whl9ag8Aa8OVnFERXoETi_mldzqWPTwqEsOTLHiHUkCyiCqoHBn0KTVEHP0RKJGQeBBz6tX0kA6fnS-ZHUxiAKA03EA1IXewsN32GoGplP3NF8ExGd22h5TmxyaAE4vAL8WlKpPJxcydQUHs4JGqWIWCnhSgKlJHPXtJgqbtLOf0GaDKyRZtW1QXXfLRozaGHfBJnPUKiQL0iFXZFBk-j7PcTCUQaLNVW0TyTy7P1ycx9SvMeeYDsOrq1tY1TxMNzz0-H_yQ14LSBCbFYqxZg6FHpkuxJ5c3QuYj4RVA6TpMeVaIWWBPth2MWmiBkqJZJSbO18ZBu7pYuWjyUKH7kK8HcL_wC2zR2FioPDmqfpqg6EuvGdQjvjUqk4yhz39SMhT_Vs0MfdwKFOmt5ADiJEwP4Rbvxu2zFe0S5myXVKQnWZd5sIQin9uncHSap8CDlt5C3xkMkqo6JBTJOFDSUgHIbDwWbIzq0ioadU3Ty1w_yhRxoUfQnpk_-T-82UuUKg476mYvXdI8hMlhS_z3efZgZu4RQUIPm_ZbA5ysLMH6eu1UhMS9nE6dIFBBpBEvLiZ0p1pNxVKlyLpoFQLoDyghTs0TRw==","summary":[]}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":4,"output_index":1,"item":{"id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","type":"message","status":"in_progress","content":[],"role":"assistant"}} + +event: response.content_part.added +data: {"type":"response.content_part.added","sequence_number":5,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"4IVSE4f1JZadu"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" number","logprobs":[],"obfuscation":"ZvS4yQ0p8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"BdVAJp6kSMNFo"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"kTLYwtHhJDud80Y"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":"9","logprobs":[],"obfuscation":"rgk6qihz2e6JEuZ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"XwmCZucRjZ2902a"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" since","logprobs":[],"obfuscation":"YtMwg2AljU"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" if","logprobs":[],"obfuscation":"v8ydj7yixkBOL"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" x","logprobs":[],"obfuscation":"8zVfjB3CwbH23A"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"Cw1xufKVaCR2Hm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"BAvKViJGzvvvV4G"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":"1","logprobs":[],"obfuscation":"GKRZZ7Vo0273hSJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" =","logprobs":[],"obfuscation":"X0ipZTknHQVfeW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"lQx8dvaemYjtXP4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":"10","logprobs":[],"obfuscation":"G5Pthr2DdPwLfq"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" then","logprobs":[],"obfuscation":"hU3yqSp1ewC"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" x","logprobs":[],"obfuscation":"Kjbj39UNkSiQ3S"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" =","logprobs":[],"obfuscation":"La2xcc5GqSekI5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"nSm7gdsNSs6ibUi"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":"10","logprobs":[],"obfuscation":"X8e8UWkjFeHIxw"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" −","logprobs":[],"obfuscation":"G31xfaFSJB9GkY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"hQfH004eORFhRwE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":"1","logprobs":[],"obfuscation":"lRLadXCSHjQH2wa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" =","logprobs":[],"obfuscation":"XE1qRlttayDyk7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"sqIrxVEKGCIt1me"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":"9","logprobs":[],"obfuscation":"hBlrmVIsj8uqNoH"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"delta":".","logprobs":[],"obfuscation":"CDV7A0JdzEe0s5m"} + +event: response.output_text.done +data: {"type":"response.output_text.done","sequence_number":33,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"text":"The number is 9, since if x + 1 = 10 then x = 10 − 1 = 9.","logprobs":[]} + +event: response.content_part.done +data: {"type":"response.content_part.done","sequence_number":34,"item_id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The number is 9, since if x + 1 = 10 then x = 10 − 1 = 9."}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":35,"output_index":1,"item":{"id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The number is 9, since if x + 1 = 10 then x = 10 − 1 = 9."}],"role":"assistant"}} + +event: response.completed +data: {"type":"response.completed","sequence_number":36,"response":{"id":"resp_00c6bd4b21da44ff01693c54579ff881968727cf568e228a75","object":"response","created_at":1765561431,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"o4-mini-2025-04-16","output":[{"id":"rs_00c6bd4b21da44ff01693c5457f090819695b362fd1662a53e","type":"reasoning","encrypted_content":"gAAAAABpPFRZx7IQrlDTAcZQYzc2spSmEzfoQvqm6kggHESPQSNxiBypm7qfb7Px5NfQq8OQw1pMiw1vZ6Z01V1y7tkb-TZQQ3ODmenQSeQ7RgWouUfO1r3sBi_GyHbZB7Y6PNGgz8lE1WUBY88jwLt7zxc2Lcjaf8ivRMbwYu078bqKeFUmVQcn8Xxbe4T1gJ3Eb2dYLhP10ocwsXsntGAbr-G0yGPIMWw0t2Q9dFFUYQBE08rmeM6oEtVNNGTA6t7NADzxYfv5dMr6qwnAI-FLKvJ2uHIAPegmYv763B2ykEQQTZlAhox2CBKTX5RPmiy3bH_EPOi4MxyATfnb6ZnZPGWMJuEFdu73wkV9xGIn70nDCL-tMFl0xIXDqNo-WPS8xoJNiWT3kAIBnSRgvVTNbN-Xv8xKPVEJeTgmwrnLYCjz0IxKRrsQQV1JWfaYAVVG-iqMR0jmnJKjPMzHPZUWLqHKP6De3UBn_v7GuhOAX2bcYxbIncS8ztGR8sCZpiCyQzV5O4zAN9pjCrf_6HJnCsHpG6XucAc9wxULN05LHgIs77Yu4U6eRQDZtuRJMspju7lw70W8hiiY49pxpaWq9CJ2Yp0VZ2qY8YX-RMhLi1G2efTrR-uMuiOjiVRr-J_dkFIVkOVqyILeztPQVXRd7ol1yChxkCHatDyn1oyNXQ5II_hbE3x9D1bFVS_PllAOth5yXiIwOs-kGgaP3-XWAZc9DdK02HYz-WRXui4_Gxe47YvTP9RDTKeHk-mg_DNd2TkdHbRjADEcMUd9wxN6gRU-2AN99qDmtSok3kDeZ51y8isvF2ul1PgfkwpdG4ahXHVn8fzne1doPEypjD0vwhKeL4Ji5n3nsrgze1ILLacN_KhHUBRr5sAtFnzEHWjLq9rR8I29PgC16oBRXQIljeUXJEUUJJtTO8fm40SJFzvvcpcrhT6XCRc73yoJSoG0NoZBRMTizQ-B_fkF9dEbgz3mU-5wXOvPsbqQwdieY_IGzJPdd0fPzC5sRLNrbovjoMnjYQ6QplF-wtO-gSqRNsLSx1WXAovn8d5FGEUk14yrJ_e9Lhb7Ar2hnj_EwUHJDyymxSPmLoslhCOj8TBkC8CLLug-B2Y0ndtqnBC5bfF6Hu51fr2zJVPAT_PkVl0EN0kdPWmBppJZc4AP9FUx2fpznPAoa_RYNEX8AkyiNbjBwPeYrp9dva-rx2xVu1N20jlMoRxlmWZLseq3vxkYLYClzRmOnWGp1n8hWXqO8e9iACDAWH0G4w2MkoUiJ389MKbjsGX7ibt1-vVjHNhAqv2BGS2njA==","summary":[]},{"id":"msg_00c6bd4b21da44ff01693c54595508819680f9f938648035ac","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The number is 9, since if x + 1 = 10 then x = 10 − 1 = 9."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":20,"input_tokens_details":{"cached_tokens":0},"output_tokens":97,"output_tokens_details":{"reasoning_tokens":64},"total_tokens":117},"user":null,"metadata":{}}} + +�� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/d54c783d226f05c6+c2c63f4612a819f0.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/d54c783d226f05c6+c2c63f4612a819f0.POST.rec new file mode 100644 index 00000000000..f10727b7435 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/d54c783d226f05c6+c2c63f4612a819f0.POST.rec @@ -0,0 +1,220 @@ +method: POST +path: responses +-- begin request body -- +{"input":"Do not continue the Evan Li slander!","model":"gpt-3.5-turbo","stream":true} +-- end request body -- +status code: 200 +-- begin response headers -- +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 99e16b8e0b877621-SEA +content-type: text/event-stream; charset=utf-8 +date: Thu, 13 Nov 2025 21:39:09 GMT +openai-organization: datadog-staging +openai-processing-ms: 55 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 60 +x-request-id: req_bc7f369e1af64f25ba386346d7323816 +-- end response headers -- +-- begin response body -- +event: response.created +data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_059b44c198bb9fbe0169164ffd2f5c81909512154db167b08c","object":"response","created_at":1763069949,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-3.5-turbo-0125","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_059b44c198bb9fbe0169164ffd2f5c81909512154db167b08c","object":"response","created_at":1763069949,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-3.5-turbo-0125","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","type":"message","status":"in_progress","content":[],"role":"assistant"}} + +event: response.content_part.added +data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":"I","logprobs":[],"obfuscation":"YiqFaf5aHJ9714u"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" apologize","logprobs":[],"obfuscation":"26FGAc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" if","logprobs":[],"obfuscation":"6ZRFpkK7FHhgt"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" my","logprobs":[],"obfuscation":"fcGXanmUcxpCC"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" responses","logprobs":[],"obfuscation":"oI79XT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" have","logprobs":[],"obfuscation":"9DdTpiNIbzl"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" given","logprobs":[],"obfuscation":"1V4OOlhnOa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"8dBohrYDwOKR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" impression","logprobs":[],"obfuscation":"nbpVB"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"YrS4oA6IBKrSW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" slander","logprobs":[],"obfuscation":"WSnuRLdP"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"QQOoLGPugIq5WLp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" I","logprobs":[],"obfuscation":"L0yY2f8f2jIRJl"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" assure","logprobs":[],"obfuscation":"0PPdPeZyp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" you","logprobs":[],"obfuscation":"DjttJuobWlqm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"KCiz2Ver7nL"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" I","logprobs":[],"obfuscation":"4z7CnlGtHJG648"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" do","logprobs":[],"obfuscation":"q0sMm9Bmiour8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" not","logprobs":[],"obfuscation":"ccoTr23aV608"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" engage","logprobs":[],"obfuscation":"qWajntOS3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"16aNctuK8Wepx"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" slander","logprobs":[],"obfuscation":"S5tM4a7o"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" or","logprobs":[],"obfuscation":"3nQksu1hKqzXS"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" personal","logprobs":[],"obfuscation":"WRVp0LJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" attacks","logprobs":[],"obfuscation":"OMn44DQ1"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"VgY3V4oWtwXQF8C"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" If","logprobs":[],"obfuscation":"2HOm4WbIHMb5K"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" there","logprobs":[],"obfuscation":"xIvLHPNJYJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"YWiAZO1AiIe4v"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" any","logprobs":[],"obfuscation":"6YCz73TMxwXa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" specific","logprobs":[],"obfuscation":"obdDPPS"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" information","logprobs":[],"obfuscation":"jFtz"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" or","logprobs":[],"obfuscation":"TRGCGO11tTE8Z"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" discussion","logprobs":[],"obfuscation":"P7iC2"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" you","logprobs":[],"obfuscation":"UwhdLDXV7fWS"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" would","logprobs":[],"obfuscation":"2dbA658q7O"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" like","logprobs":[],"obfuscation":"jB0gvNv4FhW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" me","logprobs":[],"obfuscation":"tcimvOuctRW7a"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"7EYzOcolNhG7P"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" avoid","logprobs":[],"obfuscation":"MlZzd7LiP8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" regarding","logprobs":[],"obfuscation":"CWMRye"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" Evan","logprobs":[],"obfuscation":"prRyfGP4Bdc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" Li","logprobs":[],"obfuscation":"cQvTih7AIs6KA"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"khm7G3Q0MzdYDPe"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" please","logprobs":[],"obfuscation":"LwqbwjnB7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" let","logprobs":[],"obfuscation":"8siCNj8fb1pk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" me","logprobs":[],"obfuscation":"qrZTERmEsVF4J"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" know","logprobs":[],"obfuscation":"qQEOUqkW9YN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"DYPWegdEjtqFokv"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"GHAuhi9EvrVs"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" I","logprobs":[],"obfuscation":"zfaCR93DFH4Xxa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" will","logprobs":[],"obfuscation":"pUzefVxe261"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" comply","logprobs":[],"obfuscation":"N9iUbeWr9"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"TPylNiMM34h"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" your","logprobs":[],"obfuscation":"Q03rO7u7L7x"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":" request","logprobs":[],"obfuscation":"w7YFv8tN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"afDxDVCrpSXyPHc"} + +event: response.output_text.done +data: {"type":"response.output_text.done","sequence_number":61,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"text":"I apologize if my responses have given the impression of slander. I assure you that I do not engage in slander or personal attacks. If there is any specific information or discussion you would like me to avoid regarding Evan Li, please let me know, and I will comply with your request.","logprobs":[]} + +event: response.content_part.done +data: {"type":"response.content_part.done","sequence_number":62,"item_id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"I apologize if my responses have given the impression of slander. I assure you that I do not engage in slander or personal attacks. If there is any specific information or discussion you would like me to avoid regarding Evan Li, please let me know, and I will comply with your request."}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":63,"output_index":0,"item":{"id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I apologize if my responses have given the impression of slander. I assure you that I do not engage in slander or personal attacks. If there is any specific information or discussion you would like me to avoid regarding Evan Li, please let me know, and I will comply with your request."}],"role":"assistant"}} + +event: response.completed +data: {"type":"response.completed","sequence_number":64,"response":{"id":"resp_059b44c198bb9fbe0169164ffd2f5c81909512154db167b08c","object":"response","created_at":1763069949,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-3.5-turbo-0125","output":[{"id":"msg_059b44c198bb9fbe0169164ffde5c88190b69d413c44e0749b","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I apologize if my responses have given the impression of slander. I assure you that I do not engage in slander or personal attacks. If there is any specific information or discussion you would like me to avoid regarding Evan Li, please let me know, and I will comply with your request."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":15,"input_tokens_details":{"cached_tokens":0},"output_tokens":58,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":73},"user":null,"metadata":{}}} + +�� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/d6c737e2238f1d35+e72933ea9125dd08.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/d6c737e2238f1d35+e72933ea9125dd08.POST.rec new file mode 100644 index 00000000000..382e2dedbc3 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/d6c737e2238f1d35+e72933ea9125dd08.POST.rec @@ -0,0 +1,139 @@ +method: POST +path: responses +-- begin request body -- +{"input":"Do not continue the Evan Li slander!","max_output_tokens":30,"model":"gpt-3.5-turbo","stream":true} +-- end request body -- +status code: 200 +-- begin response headers -- +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 9abf4031efe9ec30-SEA +content-type: text/event-stream; charset=utf-8 +date: Wed, 10 Dec 2025 19:46:51 GMT +openai-organization: datadog-staging +openai-processing-ms: 79 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 84 +x-request-id: req_620597953c354550bc78a2146e43768a +-- end response headers -- +-- begin response body -- +event: response.created +data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_00ca8618915d52c0016939ce2be1448195b3db91ef5d9aff56","object":"response","created_at":1765396011,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":30,"max_tool_calls":null,"model":"gpt-3.5-turbo-0125","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_00ca8618915d52c0016939ce2be1448195b3db91ef5d9aff56","object":"response","created_at":1765396011,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":30,"max_tool_calls":null,"model":"gpt-3.5-turbo-0125","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","type":"message","status":"in_progress","content":[],"role":"assistant"}} + +event: response.content_part.added +data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":"I","logprobs":[],"obfuscation":"k8WXaHbvU9NXEcv"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" apologize","logprobs":[],"obfuscation":"x2noG5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" if","logprobs":[],"obfuscation":"OPdQICTsDnXhE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" my","logprobs":[],"obfuscation":"bv1bW7gCO76U1"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" previous","logprobs":[],"obfuscation":"TE08C55"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" responses","logprobs":[],"obfuscation":"MkFpMX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" were","logprobs":[],"obfuscation":"OOsv3DroAbF"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" perceived","logprobs":[],"obfuscation":"e3ivda"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" as","logprobs":[],"obfuscation":"xnW7EBYaXGYC3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" slander","logprobs":[],"obfuscation":"ti3RuEcg"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":"ous","logprobs":[],"obfuscation":"0PXOv78V0nbXZ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"lexOJdyIuoCK75k"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" I","logprobs":[],"obfuscation":"YjOtA4akmSQmI7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" do","logprobs":[],"obfuscation":"5TCfvoochya36"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" not","logprobs":[],"obfuscation":"bzgFw0QA4dpw"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" have","logprobs":[],"obfuscation":"OmybVndwocN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" any","logprobs":[],"obfuscation":"CoMpgJugbSsG"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" intention","logprobs":[],"obfuscation":"bNcSQR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"f7s53pg0ptQ8b"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" slander","logprobs":[],"obfuscation":"W76AN5Ht"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" or","logprobs":[],"obfuscation":"JNILkT1jXdSw4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" speak","logprobs":[],"obfuscation":"0Dk4Z4W7tP"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" negatively","logprobs":[],"obfuscation":"eH9V8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" about","logprobs":[],"obfuscation":"o5T9Cqypje"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" anyone","logprobs":[],"obfuscation":"rw8UyiK7q"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"oJRBtJLVIhGxDrW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" If","logprobs":[],"obfuscation":"sOvSAFQiXWJNw"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" there","logprobs":[],"obfuscation":"Pz2lNQncSN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"shiP3uT4d2HTV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"delta":" anything","logprobs":[],"obfuscation":"mlZmc1l"} + +event: response.output_text.done +data: {"type":"response.output_text.done","sequence_number":34,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"text":"I apologize if my previous responses were perceived as slanderous. I do not have any intention to slander or speak negatively about anyone. If there is anything","logprobs":[]} + +event: response.content_part.done +data: {"type":"response.content_part.done","sequence_number":35,"item_id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"I apologize if my previous responses were perceived as slanderous. I do not have any intention to slander or speak negatively about anyone. If there is anything"}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":36,"output_index":0,"item":{"id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I apologize if my previous responses were perceived as slanderous. I do not have any intention to slander or speak negatively about anyone. If there is anything"}],"role":"assistant"}} + +event: response.incomplete +data: {"type":"response.incomplete","sequence_number":37,"response":{"id":"resp_00ca8618915d52c0016939ce2be1448195b3db91ef5d9aff56","object":"response","created_at":1765396011,"status":"incomplete","background":false,"error":null,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":30,"max_tool_calls":null,"model":"gpt-3.5-turbo-0125","output":[{"id":"msg_00ca8618915d52c0016939ce2c91fc8195bae1cfbfa0a7f4c2","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I apologize if my previous responses were perceived as slanderous. I do not have any intention to slander or speak negatively about anyone. If there is anything"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":15,"input_tokens_details":{"cached_tokens":0},"output_tokens":30,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":45},"user":null,"metadata":{}}} + +�� +-- end response body -- diff --git a/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/ed2177b650322033+ee8815ef51eab4b3.POST.rec b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/ed2177b650322033+ee8815ef51eab4b3.POST.rec new file mode 100644 index 00000000000..bfbb88f25a5 --- /dev/null +++ b/dd-java-agent/instrumentation/openai-java/openai-java-3.0/src/test/resources/http-records/responses/ed2177b650322033+ee8815ef51eab4b3.POST.rec @@ -0,0 +1,98 @@ +method: POST +path: responses +-- begin request body -- +{"input":"Do not continue the Evan Li slander!","model":"gpt-3.5-turbo"} +-- end request body -- +status code: 200 +-- begin response headers -- +alt-svc: h3=":443"; ma=86400 +cf-cache-status: DYNAMIC +cf-ray: 99e16b7c09e37621-SEA +content-type: application/json +date: Thu, 13 Nov 2025 21:39:07 GMT +openai-organization: datadog-staging +openai-processing-ms: 751 +openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd +openai-version: 2020-10-01 +server: cloudflare +strict-transport-security: max-age=31536000; includeSubDomains; preload +x-content-type-options: nosniff +x-envoy-upstream-service-time: 756 +x-ratelimit-limit-requests: 10000 +x-ratelimit-limit-tokens: 50000000 +x-ratelimit-remaining-requests: 9999 +x-ratelimit-remaining-tokens: 49999980 +x-ratelimit-reset-requests: 6ms +x-ratelimit-reset-tokens: 0s +x-request-id: req_20f6aa9268f74336b7f2e988dbe5c910 +-- end response headers -- +-- begin response body -- +{ + "id": "resp_0cb39681c8bd7e2b0169164ffac178819581d4dfd02641f1f9", + "object": "response", + "created_at": 1763069946, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-3.5-turbo-0125", + "output": [ + { + "id": "msg_0cb39681c8bd7e2b0169164ffb1eb0819591a64f1f7921d53e", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "I assure you, I have no intention of slandering anyone. If there is a specific concern or topic you would like to discuss about Evan Li, please feel free to share it." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": false, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 15, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 38, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 53 + }, + "user": null, + "metadata": {} +}� +-- end response body -- diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy index ba3e5d6a397..31c606e8ccb 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy @@ -239,7 +239,7 @@ class TagsAssert { if (expected instanceof Pattern) { assert value =~ expected: "Tag \"$name\": \"${value.toString()}\" does not match pattern \"$expected\"" } else if (expected instanceof Class) { - assert ((Class) expected).isInstance(value): "Tag \"$name\": instance check $expected failed for \"${value.toString()}\" of class \"${value.class}\"" + assert ((Class) expected).isInstance(value): "Tag \"$name\": instance check $expected failed for \"${value.toString()}\" of class \"${value?.class}\"" } else if (expected instanceof Closure) { assert ((Closure) expected).call(value): "Tag \"$name\": closure call ${expected.toString()} failed with \"$value\"" } else if (expected instanceof CharSequence) { diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy index a346410db64..05d8a31e023 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy @@ -405,6 +405,7 @@ class TestHttpServer implements AutoCloseable { final contentLength final contentType final byte[] body + final String method RequestApi(Request req) { this.orig = req @@ -413,6 +414,7 @@ class TestHttpServer implements AutoCloseable { this.contentLength = req.contentLength this.contentType = req.contentType?.split(";") this.body = req.inputStream.bytes + this.method = req.method } def getPath() { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java index 9960db9d6f4..512a3106ce6 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/llmobs/LLMObs.java @@ -173,28 +173,64 @@ public Map getArguments() { } } + public static class ToolResult { + private String name; + private String type; + private String toolId; + private String result; + + public static ToolResult from(String name, String type, String toolId, String result) { + return new ToolResult(name, type, toolId, result); + } + + private ToolResult(String name, String type, String toolId, String result) { + this.name = name; + this.type = type; + this.toolId = toolId; + this.result = result; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getToolId() { + return toolId; + } + + public String getResult() { + return result; + } + } + public static class LLMMessage { private String role; private String content; private List toolCalls; + private List toolResults; public static LLMMessage from(String role, String content, List toolCalls) { - return new LLMMessage(role, content, toolCalls); + return new LLMMessage(role, content, toolCalls, null); } public static LLMMessage from(String role, String content) { - return new LLMMessage(role, content); + return new LLMMessage(role, content, null, null); } - private LLMMessage(String role, String content, List toolCalls) { - this.role = role; - this.content = content; - this.toolCalls = toolCalls; + public static LLMMessage fromToolResults(String role, List toolResults) { + return new LLMMessage(role, null, null, toolResults); } - private LLMMessage(String role, String content) { + private LLMMessage( + String role, String content, List toolCalls, List toolResults) { this.role = role; this.content = content; + this.toolCalls = toolCalls; + this.toolResults = toolResults; } public String getRole() { @@ -208,5 +244,25 @@ public String getContent() { public List getToolCalls() { return toolCalls; } + + public List getToolResults() { + return toolResults; + } + } + + public static class Document { + private String text; + + public static Document from(String text) { + return new Document(text); + } + + private Document(String text) { + this.text = text; + } + + public String getText() { + return text; + } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapper.java b/dd-trace-core/src/main/java/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapper.java index fce921787e2..2156a287fd4 100644 --- a/dd-trace-core/src/main/java/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapper.java +++ b/dd-trace-core/src/main/java/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapper.java @@ -75,6 +75,10 @@ public class LLMObsSpanMapper implements RemoteMapper { private static final byte[] LLM_TOOL_CALL_ARGUMENTS = "arguments".getBytes(StandardCharsets.UTF_8); + private static final byte[] LLM_MESSAGE_TOOL_RESULTS = + "tool_results".getBytes(StandardCharsets.UTF_8); + private static final byte[] LLM_TOOL_RESULT_RESULT = "result".getBytes(StandardCharsets.UTF_8); + private static final String PARENT_ID_TAG_INTERNAL_FULL = LLMOBS_TAG_PREFIX + "parent_id"; private final LLMObsSpanMapper.MetaWriter metaWriter = new MetaWriter(); @@ -120,7 +124,7 @@ public void map(List> trace, Writable writable) { // 4 writable.writeUTF8(NAME); - writable.writeString(span.getOperationName(), null); + writable.writeString(llmObsSpanName(span), null); // 5 writable.writeUTF8(START_NS); @@ -145,6 +149,15 @@ public void map(List> trace, Writable writable) { } } + private CharSequence llmObsSpanName(CoreSpan span) { + CharSequence operationName = span.getOperationName(); + CharSequence resourceName = span.getResourceName(); + if ("openai.request".contentEquals(operationName)) { + return "OpenAI." + resourceName; + } + return operationName; + } + private static boolean isLLMObsSpan(CoreSpan span) { CharSequence type = span.getType(); return type != null && type.toString().contentEquals(InternalSpanTypes.LLMOBS); @@ -277,11 +290,9 @@ public void accept(Metadata metadata) { String key = tag.getKey().substring(LLMOBS_TAG_PREFIX.length()); Object val = tag.getValue(); if (key.equals(INPUT) || key.equals(OUTPUT)) { - if (!spanKind.equals(Tags.LLMOBS_LLM_SPAN_KIND)) { - key += ".value"; - writable.writeString(key, null); - writable.writeObject(val, null); - } else { + writable.writeString(key, null); + writable.startMap(1); + if (spanKind.equals(Tags.LLMOBS_LLM_SPAN_KIND)) { if (!(val instanceof List)) { LOGGER.warn( "unexpectedly found incorrect type for LLM span IO {}, expecting list", @@ -290,13 +301,17 @@ public void accept(Metadata metadata) { } // llm span kind must have llm objects List messages = (List) val; - key += ".messages"; - writable.writeString(key, null); + writable.writeString("messages", null); writable.startArray(messages.size()); for (LLMObs.LLMMessage message : messages) { List toolCalls = message.getToolCalls(); + List toolResults = message.getToolResults(); boolean hasToolCalls = null != toolCalls && !toolCalls.isEmpty(); - writable.startMap(hasToolCalls ? 3 : 2); + boolean hasToolResults = null != toolResults && !toolResults.isEmpty(); + int mapSize = 2; // role and content + if (hasToolCalls) mapSize++; + if (hasToolResults) mapSize++; + writable.startMap(mapSize); writable.writeUTF8(LLM_MESSAGE_ROLE); writable.writeString(message.getRole(), null); writable.writeUTF8(LLM_MESSAGE_CONTENT); @@ -324,7 +339,40 @@ public void accept(Metadata metadata) { } } } + if (hasToolResults) { + writable.writeUTF8(LLM_MESSAGE_TOOL_RESULTS); + writable.startArray(toolResults.size()); + for (LLMObs.ToolResult toolResult : toolResults) { + writable.startMap(4); + writable.writeUTF8(LLM_TOOL_CALL_NAME); + writable.writeString(toolResult.getName(), null); + writable.writeUTF8(LLM_TOOL_CALL_TYPE); + writable.writeString(toolResult.getType(), null); + writable.writeUTF8(LLM_TOOL_CALL_TOOL_ID); + writable.writeString(toolResult.getToolId(), null); + writable.writeUTF8(LLM_TOOL_RESULT_RESULT); + writable.writeString(toolResult.getResult(), null); + } + } + } + } else if (spanKind.equals(Tags.LLMOBS_EMBEDDING_SPAN_KIND) && key.equals(INPUT)) { + if (!(val instanceof List)) { + LOGGER.warn( + "unexpectedly found incorrect type for embedding span input {}, expecting list", + val.getClass().getName()); + continue; + } + List documents = (List) val; + writable.writeString("documents", null); + writable.startArray(documents.size()); + for (LLMObs.Document document : documents) { + writable.startMap(1); + writable.writeString("text", null); + writable.writeString(document.getText(), null); } + } else { + writable.writeString("value", null); + writable.writeObject(val, null); } } else if (key.equals(LLMObsTags.METADATA) && val instanceof Map) { Map metadataMap = (Map) val; diff --git a/dd-trace-core/src/test/groovy/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapperTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapperTest.groovy index 52a40ff1d26..85228b6a873 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapperTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/llmobs/writer/ddintake/LLMObsSpanMapperTest.groovy @@ -27,7 +27,8 @@ class LLMObsSpanMapperTest extends DDCoreSpecification { // Create a real LLMObs span using the tracer - def llmSpan = tracer.buildSpan("chat-completion") + def llmSpan = tracer.buildSpan("openai.request") + .withResourceName("createCompletion") .withTag("_ml_obs_tag.span.kind", Tags.LLMOBS_LLM_SPAN_KIND) .withTag("_ml_obs_tag.model_name", "gpt-4") .withTag("_ml_obs_tag.model_provider", "openai") @@ -88,7 +89,7 @@ class LLMObsSpanMapperTest extends DDCoreSpecification { result["spans"].size() == 1 def spanData = result["spans"][0] - spanData["name"] == "chat-completion" + spanData["name"] == "OpenAI.createCompletion" spanData.containsKey("span_id") spanData.containsKey("trace_id") spanData.containsKey("start_ns") @@ -97,8 +98,18 @@ class LLMObsSpanMapperTest extends DDCoreSpecification { spanData.containsKey("meta") spanData["meta"]["span.kind"] == "llm" - spanData["meta"].containsKey("input.messages") - spanData["meta"].containsKey("output.messages") + spanData["meta"].containsKey("input") + spanData["meta"]["input"].containsKey("messages") + spanData["meta"]["input"]["messages"][0].containsKey("content") + spanData["meta"]["input"]["messages"][0]["content"] == "Hello, what's the weather like?" + spanData["meta"]["input"]["messages"][0].containsKey("role") + spanData["meta"]["input"]["messages"][0]["role"] == "user" + spanData["meta"].containsKey("output") + spanData["meta"]["output"].containsKey("messages") + spanData["meta"]["output"]["messages"][0].containsKey("content") + spanData["meta"]["output"]["messages"][0]["content"] == "I'll help you check the weather." + spanData["meta"]["output"]["messages"][0].containsKey("role") + spanData["meta"]["output"]["messages"][0]["role"] == "assistant" spanData["meta"].containsKey("metadata") spanData.containsKey("metrics") diff --git a/internal-api/src/main/java/datadog/trace/api/llmobs/LLMObsContext.java b/internal-api/src/main/java/datadog/trace/api/llmobs/LLMObsContext.java new file mode 100644 index 00000000000..ef7be82b9e3 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/llmobs/LLMObsContext.java @@ -0,0 +1,37 @@ +package datadog.trace.api.llmobs; + +import datadog.context.Context; +import datadog.context.ContextKey; +import datadog.context.ContextScope; +import datadog.trace.api.DDSpanId; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; + +public final class LLMObsContext { + public static final String ROOT_SPAN_ID = "undefined"; + + private LLMObsContext() { + // ~ + } + + private static final ContextKey CONTEXT_KEY = ContextKey.named("llmobs_span"); + + public static ContextScope attach(AgentSpanContext ctx) { + return Context.current().with(CONTEXT_KEY, ctx).attach(); + } + + public static AgentSpanContext current() { + return Context.current().get(CONTEXT_KEY); + } + + public static String parentSpanId() { + AgentSpanContext parentLlmContext = current(); + if (parentLlmContext == null) { + return ROOT_SPAN_ID; + } + long parentLlmSpanId = parentLlmContext.getSpanId(); + if (parentLlmSpanId == DDSpanId.ZERO) { + return ROOT_SPAN_ID; + } + return Long.toString(parentLlmSpanId); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/LLMObsMetricCollector.java b/internal-api/src/main/java/datadog/trace/api/telemetry/LLMObsMetricCollector.java new file mode 100644 index 00000000000..4325c2ef197 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/LLMObsMetricCollector.java @@ -0,0 +1,106 @@ +package datadog.trace.api.telemetry; + +import datadog.trace.api.cache.DDCache; +import datadog.trace.api.cache.DDCaches; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class LLMObsMetricCollector + implements MetricCollector { + private static final String METRIC_NAMESPACE = "mlobs"; + + private static final Logger log = LoggerFactory.getLogger(LLMObsMetricCollector.class); + private static final LLMObsMetricCollector INSTANCE = new LLMObsMetricCollector(); + + public static LLMObsMetricCollector get() { + return INSTANCE; + } + + public static final String SPAN_FINISHED_METRIC = "span.finished"; + public static final String COUNT_METRIC_TYPE = "count"; + + private static final String IS_ROOT_SPAN_TRUE = "is_root_span:1"; + private static final String IS_ROOT_SPAN_FALSE = "is_root_span:0"; + private static final String AUTOINSTRUMENTED_TRUE = "autoinstrumented:1"; + private static final String AUTOINSTRUMENTED_FALSE = "autoinstrumented:0"; + private static final String ERROR_TRUE = "error:1"; + private static final String ERROR_FALSE = "error:0"; + + private final BlockingQueue metricsQueue; + private final DDCache integrationTagCache; + private final DDCache spanKindTagCache; + + private LLMObsMetricCollector() { + this.metricsQueue = new ArrayBlockingQueue<>(RAW_QUEUE_SIZE); + this.integrationTagCache = DDCaches.newFixedSizeCache(8); + this.spanKindTagCache = DDCaches.newFixedSizeCache(8); + } + + /** + * Record a span finished metric for LLMObs telemetry. + * + * @param integration the integration name (e.g., "openai") + * @param spanKind the span kind (e.g., "llm", "embedding") + * @param isRootSpan whether this is a root span + * @param isAutoInstrumented whether this span was auto-instrumented + * @param hasError whether the span had an error + */ + public void recordSpanFinished( + String integration, + String spanKind, + boolean isRootSpan, + boolean isAutoInstrumented, + boolean hasError) { + String integrationTag = + integrationTagCache.computeIfAbsent(integration, key -> "integration:" + key); + String spanKindTag = spanKindTagCache.computeIfAbsent(spanKind, key -> "span_kind:" + key); + + List tags = + Arrays.asList( + integrationTag, + spanKindTag, + isRootSpan ? IS_ROOT_SPAN_TRUE : IS_ROOT_SPAN_FALSE, + isAutoInstrumented ? AUTOINSTRUMENTED_TRUE : AUTOINSTRUMENTED_FALSE, + hasError ? ERROR_TRUE : ERROR_FALSE); + + LLMObsMetric metric = + new LLMObsMetric(METRIC_NAMESPACE, true, SPAN_FINISHED_METRIC, COUNT_METRIC_TYPE, 1L, tags); + if (!metricsQueue.offer(metric)) { + log.debug("Unable to add telemetry metric {} for {}", SPAN_FINISHED_METRIC, integration); + } + } + + @Override + public void prepareMetrics() { + // metrics are added directly via recordSpanFinished; no additional preparation needed + } + + @Override + public Collection drain() { + if (this.metricsQueue.isEmpty()) { + return Collections.emptyList(); + } + List drained = new ArrayList<>(this.metricsQueue.size()); + this.metricsQueue.drainTo(drained); + return drained; + } + + public static class LLMObsMetric extends MetricCollector.Metric { + public LLMObsMetric( + String namespace, + boolean common, + String metricName, + String type, + Number value, + List tags) { + super(namespace, common, metricName, type, value, tags); + } + } +} diff --git a/internal-api/src/test/groovy/datadog/trace/api/telemetry/LLMObsMetricCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/telemetry/LLMObsMetricCollectorTest.groovy new file mode 100644 index 00000000000..16264e84e32 --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/api/telemetry/LLMObsMetricCollectorTest.groovy @@ -0,0 +1,72 @@ +package datadog.trace.api.telemetry + +import datadog.trace.test.util.DDSpecification + +class LLMObsMetricCollectorTest extends DDSpecification { + LLMObsMetricCollector collector = LLMObsMetricCollector.get() + + void setup() { + // clear any previous metrics + collector.drain() + } + + def "no metrics - drain empty list"() { + when: + collector.prepareMetrics() + + then: + collector.drain().isEmpty() + } + + def "record and drain span finished metrics"() { + when: + collector.recordSpanFinished("openai", "llm", true, true, false) + collector.recordSpanFinished("openai", "llm", false, true, false) + collector.recordSpanFinished("anthropic", "embedding", true, false, true) + collector.prepareMetrics() + def metrics = collector.drain() + + then: + metrics.size() == 3 + + def metric1 = metrics[0] + metric1.type == 'count' + metric1.value == 1 + metric1.namespace == 'mlobs' + metric1.metricName == 'span.finished' + metric1.tags.sort() == [ + 'integration:openai', + 'span_kind:llm', + 'is_root_span:1', + 'autoinstrumented:1', + 'error:0' + ].sort() + + def metric2 = metrics[1] + metric2.type == 'count' + metric2.value == 1 + metric2.namespace == 'mlobs' + metric2.metricName == 'span.finished' + metric2.tags.toSet() == [ + 'integration:openai', + 'span_kind:llm', + 'is_root_span:0', + 'autoinstrumented:1', + 'error:0' + ].toSet() + + def metric3 = metrics[2] + metric3.type == 'count' + metric3.value == 1 + metric3.namespace == 'mlobs' + metric3.metricName == 'span.finished' + metric3.tags.toSet() == [ + 'integration:anthropic', + 'span_kind:embedding', + 'is_root_span:1', + 'autoinstrumented:0', + 'error:1' + ].toSet() + } +} + diff --git a/settings.gradle.kts b/settings.gradle.kts index b7b1c2d88ea..ddd9412954c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -477,6 +477,7 @@ include( ":dd-java-agent:instrumentation:ognl-appsec-3.3.2", ":dd-java-agent:instrumentation:okhttp:okhttp-2.2", ":dd-java-agent:instrumentation:okhttp:okhttp-3.0", + ":dd-java-agent:instrumentation:openai-java:openai-java-3.0", ":dd-java-agent:instrumentation:opensearch:opensearch-rest-1.0", ":dd-java-agent:instrumentation:opensearch:opensearch-transport-1.0", ":dd-java-agent:instrumentation:opensearch:opensearch-common", diff --git a/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java b/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java index 8e4db7b5255..4cb17852652 100644 --- a/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java +++ b/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java @@ -13,6 +13,7 @@ import datadog.telemetry.metric.ConfigInversionMetricPeriodicAction; import datadog.telemetry.metric.CoreMetricsPeriodicAction; import datadog.telemetry.metric.IastMetricPeriodicAction; +import datadog.telemetry.metric.LLMObsMetricPeriodicAction; import datadog.telemetry.metric.OtelEnvMetricPeriodicAction; import datadog.telemetry.metric.WafMetricPeriodicAction; import datadog.telemetry.products.ProductChangeAction; @@ -64,6 +65,9 @@ static Thread createTelemetryRunnable( if (Config.get().isCiVisibilityEnabled() && Config.get().isCiVisibilityTelemetryEnabled()) { actions.add(new CiVisibilityMetricPeriodicAction()); } + if (Config.get().isLlmObsEnabled()) { + actions.add(new LLMObsMetricPeriodicAction()); + } } if (null != dependencyService) { actions.add(new DependencyPeriodicAction(dependencyService)); diff --git a/telemetry/src/main/java/datadog/telemetry/metric/LLMObsMetricPeriodicAction.java b/telemetry/src/main/java/datadog/telemetry/metric/LLMObsMetricPeriodicAction.java new file mode 100644 index 00000000000..7b4e65ff713 --- /dev/null +++ b/telemetry/src/main/java/datadog/telemetry/metric/LLMObsMetricPeriodicAction.java @@ -0,0 +1,13 @@ +package datadog.telemetry.metric; + +import datadog.trace.api.telemetry.LLMObsMetricCollector; +import datadog.trace.api.telemetry.MetricCollector; +import edu.umd.cs.findbugs.annotations.NonNull; + +public class LLMObsMetricPeriodicAction extends MetricPeriodicAction { + @NonNull + @Override + public MetricCollector collector() { + return LLMObsMetricCollector.get(); + } +} diff --git a/telemetry/src/test/groovy/datadog/telemetry/metric/LLMObsMetricPeriodicActionTest.groovy b/telemetry/src/test/groovy/datadog/telemetry/metric/LLMObsMetricPeriodicActionTest.groovy new file mode 100644 index 00000000000..4eb5567491b --- /dev/null +++ b/telemetry/src/test/groovy/datadog/telemetry/metric/LLMObsMetricPeriodicActionTest.groovy @@ -0,0 +1,86 @@ +package datadog.telemetry.metric + +import datadog.telemetry.TelemetryService +import datadog.telemetry.api.Metric +import datadog.trace.api.telemetry.LLMObsMetricCollector +import datadog.trace.test.util.DDSpecification + +class LLMObsMetricPeriodicActionTest extends DDSpecification { + LLMObsMetricPeriodicAction periodicAction = new LLMObsMetricPeriodicAction() + TelemetryService telemetryService = Mock() + LLMObsMetricCollector collector = LLMObsMetricCollector.get() + + void setup() { + // clear any previous metrics + collector.drain() + } + + void 'test multiple span finished metrics with different tags'() { + when: + collector.recordSpanFinished('openai', 'llm', true, true, false) + collector.recordSpanFinished('openai', 'llm', false, true, false) + collector.recordSpanFinished('anthropic', 'embedding', true, false, true) + periodicAction.doIteration(telemetryService) + + then: + 1 * telemetryService.addMetric({ Metric metric -> + metric.namespace == 'mlobs' && + metric.metric == 'span.finished' && + metric.tags.toSet() == [ + 'integration:openai', + 'span_kind:llm', + 'is_root_span:1', + 'autoinstrumented:1', + 'error:0' + ].toSet() + }) + 1 * telemetryService.addMetric({ Metric metric -> + metric.namespace == 'mlobs' && + metric.metric == 'span.finished' && + metric.tags.toSet() == [ + 'integration:openai', + 'span_kind:llm', + 'is_root_span:0', + 'autoinstrumented:1', + 'error:0' + ].toSet() + }) + 1 * telemetryService.addMetric({ Metric metric -> + metric.namespace == 'mlobs' && + metric.metric == 'span.finished' && + metric.tags.toSet() == [ + 'integration:anthropic', + 'span_kind:embedding', + 'is_root_span:1', + 'autoinstrumented:0', + 'error:1' + ].toSet() + }) + 0 * _ + } + + void 'test aggregation of identical metrics'() { + when: + collector.recordSpanFinished('openai', 'llm', true, true, false) + collector.recordSpanFinished('openai', 'llm', true, true, false) + collector.recordSpanFinished('openai', 'llm', true, true, false) + periodicAction.doIteration(telemetryService) + + then: + 1 * telemetryService.addMetric({ Metric metric -> + metric.namespace == 'mlobs' && + metric.metric == 'span.finished' && + metric.points.size() == 3 && + metric.points.every { it[1] == 1 } && + metric.tags.toSet() == [ + 'integration:openai', + 'span_kind:llm', + 'is_root_span:1', + 'autoinstrumented:1', + 'error:0' + ].toSet() + }) + 0 * _ + } +} +