From 8b9b4d08bfd5882e91fc5d5ccc7a5fdc5bb2aaa6 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 18 Jul 2025 16:26:26 +0900 Subject: [PATCH 1/3] Instrument streaming chat for openai SDK --- .../InstrumentedChatCompletionService.java | 67 +- .../openai/v1_1/StreamedMessageBuffer.java | 144 +++ .../openai/v1_1/TracingStreamedResponse.java | 180 ++++ .../instrumentation/openai/v1_1/ChatTest.java | 483 ++++++++++ .../openai/v1_1/AbstractChatTest.java | 642 +++++++++++++ ...n.openai.v1_1.abstractchattest.stream.yaml | 59 ++ ...1.abstractchattest.streamincludeusage.yaml | 64 ++ ...bstractchattest.streammultiplechoices.yaml | 72 ++ ...v1_1.abstractchattest.streamtoolcalls.yaml | 856 ++++++++++++++++++ 9 files changed, 2558 insertions(+), 9 deletions(-) create mode 100644 instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/StreamedMessageBuffer.java create mode 100644 instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/TracingStreamedResponse.java create mode 100644 instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.stream.yaml create mode 100644 instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamincludeusage.yaml create mode 100644 instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streammultiplechoices.yaml create mode 100644 instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamtoolcalls.yaml diff --git a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java index e62be2545003..9c2e338efa52 100644 --- a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java +++ b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java @@ -6,7 +6,9 @@ package io.opentelemetry.instrumentation.openai.v1_1; import com.openai.core.RequestOptions; +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 com.openai.services.blocking.chat.ChatCompletionService; import io.opentelemetry.api.logs.Logger; @@ -43,14 +45,27 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl String methodName = method.getName(); Class[] parameterTypes = method.getParameterTypes(); - if (methodName.equals("create") - && parameterTypes.length >= 1 - && parameterTypes[0] == ChatCompletionCreateParams.class) { - if (parameterTypes.length == 1) { - return create((ChatCompletionCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 && parameterTypes[1] == RequestOptions.class) { - return create((ChatCompletionCreateParams) args[0], (RequestOptions) args[1]); - } + switch (methodName) { + case "create": + if (parameterTypes.length >= 1 && parameterTypes[0] == ChatCompletionCreateParams.class) { + if (parameterTypes.length == 1) { + return create((ChatCompletionCreateParams) args[0], RequestOptions.none()); + } else if (parameterTypes.length == 2 && parameterTypes[1] == RequestOptions.class) { + return create((ChatCompletionCreateParams) args[0], (RequestOptions) args[1]); + } + } + break; + case "createStreaming": + if (parameterTypes.length >= 1 && parameterTypes[0] == ChatCompletionCreateParams.class) { + if (parameterTypes.length == 1) { + return createStreaming((ChatCompletionCreateParams) args[0], RequestOptions.none()); + } else if (parameterTypes.length == 2 && parameterTypes[1] == RequestOptions.class) { + return createStreaming((ChatCompletionCreateParams) args[0], (RequestOptions) args[1]); + } + } + break; + default: + // fallthrough } return super.invoke(proxy, method, args); @@ -58,7 +73,6 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl private ChatCompletion create( ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { - Context parentCtx = Context.current(); if (!instrumenter.shouldStart(parentCtx, chatCompletionCreateParams)) { return createWithLogs(chatCompletionCreateParams, requestOptions); @@ -85,4 +99,39 @@ private ChatCompletion createWithLogs( ChatCompletionEventsHelper.emitCompletionLogEvents(eventLogger, result, captureMessageContent); return result; } + + public StreamResponse createStreaming( + ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { + Context parentCtx = Context.current(); + if (!instrumenter.shouldStart(parentCtx, chatCompletionCreateParams)) { + return createStreamingWithLogs(chatCompletionCreateParams, requestOptions, parentCtx, false); + } + + Context ctx = instrumenter.start(parentCtx, chatCompletionCreateParams); + try (Scope ignored = ctx.makeCurrent()) { + return createStreamingWithLogs(chatCompletionCreateParams, requestOptions, ctx, true); + } catch (Throwable t) { + instrumenter.end(ctx, chatCompletionCreateParams, null, t); + throw t; + } + } + + private StreamResponse createStreamingWithLogs( + ChatCompletionCreateParams chatCompletionCreateParams, + RequestOptions requestOptions, + Context parentCtx, + boolean newSpan) { + ChatCompletionEventsHelper.emitPromptLogEvents( + eventLogger, chatCompletionCreateParams, captureMessageContent); + StreamResponse result = + delegate.createStreaming(chatCompletionCreateParams, requestOptions); + return new TracingStreamedResponse( + result, + parentCtx, + chatCompletionCreateParams, + instrumenter, + eventLogger, + captureMessageContent, + newSpan); + } } diff --git a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/StreamedMessageBuffer.java b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/StreamedMessageBuffer.java new file mode 100644 index 000000000000..32615f07f424 --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/StreamedMessageBuffer.java @@ -0,0 +1,144 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.openai.v1_1; + +import com.openai.core.JsonField; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionChunk; +import com.openai.models.chat.completions.ChatCompletionMessage; +import io.opentelemetry.api.common.Value; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +final class StreamedMessageBuffer { + private final long index; + private final boolean captureMessageContent; + + @Nullable String finishReason; + + @Nullable private StringBuilder message; + @Nullable private Map toolCalls; + + StreamedMessageBuffer(long index, boolean captureMessageContent) { + this.index = index; + this.captureMessageContent = captureMessageContent; + } + + ChatCompletion.Choice toChoice() { + ChatCompletion.Choice.Builder choice = + ChatCompletion.Choice.builder().index(index).logprobs(Optional.empty()); + if (finishReason != null) { + choice.finishReason(ChatCompletion.Choice.FinishReason.of(finishReason)); + } else { + // Can't happen in practice, mostly to satisfy null check + choice.finishReason(JsonField.ofNullable(null)); + } + if (message != null) { + choice.message( + ChatCompletionMessage.builder() + .content(message.toString()) + .refusal(Optional.empty()) + .build()); + } else { + choice.message(JsonField.ofNullable(null)); + } + return choice.build(); + } + + Value toEventBody() { + Map> body = new HashMap<>(); + if (message != null) { + body.put("content", Value.of(message.toString())); + } + if (toolCalls != null) { + List> toolCallsJson = + toolCalls.values().stream() + .map(StreamedMessageBuffer::buildToolCallEventObject) + .collect(Collectors.toList()); + body.put("tool_calls", Value.of(toolCallsJson)); + } + return Value.of(body); + } + + void append(ChatCompletionChunk.Choice.Delta delta) { + if (captureMessageContent) { + if (delta.content().isPresent()) { + if (message == null) { + message = new StringBuilder(); + } + message.append(delta.content().get()); + } + } + + if (delta.toolCalls().isPresent()) { + if (toolCalls == null) { + toolCalls = new HashMap<>(); + } + + for (ChatCompletionChunk.Choice.Delta.ToolCall toolCall : delta.toolCalls().get()) { + ToolCallBuffer buffer = + toolCalls.computeIfAbsent( + toolCall.index(), unused -> new ToolCallBuffer(toolCall.id().orElse(""))); + toolCall.type().ifPresent(type -> buffer.type = type.toString()); + toolCall + .function() + .ifPresent( + function -> { + function.name().ifPresent(name -> buffer.function.name = name); + if (captureMessageContent) { + function + .arguments() + .ifPresent( + args -> { + if (buffer.function.arguments == null) { + buffer.function.arguments = new StringBuilder(); + } + buffer.function.arguments.append(args); + }); + } + }); + } + } + } + + private static Value buildToolCallEventObject(ToolCallBuffer call) { + Map> result = new HashMap<>(); + result.put("id", Value.of(call.id)); + if (call.type != null) { + result.put("type", Value.of(call.type)); + } + + Map> function = new HashMap<>(); + if (call.function.name != null) { + function.put("name", Value.of(call.function.name)); + } + if (call.function.arguments != null) { + function.put("arguments", Value.of(call.function.arguments.toString())); + } + result.put("function", Value.of(function)); + + return Value.of(result); + } + + private static class FunctionBuffer { + @Nullable String name; + @Nullable StringBuilder arguments; + } + + private static class ToolCallBuffer { + final String id; + final FunctionBuffer function = new FunctionBuffer(); + @Nullable String type; + + ToolCallBuffer(String id) { + this.id = id; + } + } +} diff --git a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/TracingStreamedResponse.java b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/TracingStreamedResponse.java new file mode 100644 index 000000000000..e5793c4ed840 --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/TracingStreamedResponse.java @@ -0,0 +1,180 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.openai.v1_1; + +import com.openai.core.JsonField; +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 com.openai.models.completions.CompletionUsage; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; + +final class TracingStreamedResponse implements StreamResponse { + + private final StreamResponse delegate; + private final Context parentCtx; + private final ChatCompletionCreateParams request; + private final List choiceBuffers; + + private final Instrumenter instrumenter; + private final Logger eventLogger; + private final boolean captureMessageContent; + private final boolean newSpan; + + @Nullable private CompletionUsage usage; + @Nullable private String model; + @Nullable private String responseId; + private boolean hasEnded = false; + + TracingStreamedResponse( + StreamResponse delegate, + Context parentCtx, + ChatCompletionCreateParams request, + Instrumenter instrumenter, + Logger eventLogger, + boolean captureMessageContent, + boolean newSpan) { + this.delegate = delegate; + this.parentCtx = parentCtx; + this.request = request; + this.instrumenter = instrumenter; + this.eventLogger = eventLogger; + this.captureMessageContent = captureMessageContent; + this.newSpan = newSpan; + choiceBuffers = new ArrayList<>(); + } + + @Override + public Stream stream() { + return StreamSupport.stream(new TracingSpliterator(delegate.stream().spliterator()), false); + } + + @Override + public void close() { + endSpan(); + delegate.close(); + } + + private synchronized void endSpan() { + if (hasEnded) { + return; + } + hasEnded = true; + + ChatCompletion.Builder result = + ChatCompletion.builder() + .created(0) + .choices( + choiceBuffers.stream() + .map(StreamedMessageBuffer::toChoice) + .collect(Collectors.toList())); + if (model != null) { + result.model(model); + } else { + result.model(JsonField.ofNullable(null)); + } + if (responseId != null) { + result.id(responseId); + } else { + result.id(JsonField.ofNullable(null)); + } + if (usage != null) { + result.usage(usage); + } + + if (newSpan) { + instrumenter.end(parentCtx, request, result.build(), null); + } + } + + private class TracingSpliterator implements Spliterator { + + private final Spliterator delegateSpliterator; + + private TracingSpliterator(Spliterator delegateSpliterator) { + this.delegateSpliterator = delegateSpliterator; + } + + @Override + public boolean tryAdvance(Consumer action) { + boolean chunkReceived = + delegateSpliterator.tryAdvance( + chunk -> { + model = chunk.model(); + responseId = chunk.id(); + chunk.usage().ifPresent(u -> usage = u); + + for (ChatCompletionChunk.Choice choice : chunk.choices()) { + while (choiceBuffers.size() <= choice.index()) { + choiceBuffers.add(null); + } + StreamedMessageBuffer buffer = choiceBuffers.get((int) choice.index()); + if (buffer == null) { + buffer = new StreamedMessageBuffer(choice.index(), captureMessageContent); + choiceBuffers.set((int) choice.index(), buffer); + } + buffer.append(choice.delta()); + if (choice.finishReason().isPresent()) { + buffer.finishReason = choice.finishReason().get().toString(); + + // message has ended, let's emit + ChatCompletionEventsHelper.emitCompletionLogEvent( + eventLogger, + choice.index(), + buffer.finishReason, + buffer.toEventBody(), + parentCtx); + } + } + + action.accept(chunk); + }); + if (!chunkReceived) { + endSpan(); + } + return chunkReceived; + } + + @Override + @Nullable + public Spliterator trySplit() { + // do not support parallelism to reliably catch the last chunk + return null; + } + + @Override + public long estimateSize() { + return delegateSpliterator.estimateSize(); + } + + @Override + public long getExactSizeIfKnown() { + return delegateSpliterator.getExactSizeIfKnown(); + } + + @Override + public int characteristics() { + return delegateSpliterator.characteristics(); + } + + @Override + public Comparator getComparator() { + return delegateSpliterator.getComparator(); + } + } +} diff --git a/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java b/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java index c8fdf30432cd..5b8e78edb066 100644 --- a/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java +++ b/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java @@ -20,12 +20,15 @@ import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiSystemIncubatingValues.OPENAI; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiTokenTypeIncubatingValues.COMPLETION; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiTokenTypeIncubatingValues.INPUT; +import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import com.openai.client.OpenAIClient; +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 com.openai.models.chat.completions.ChatCompletionMessageParam; import com.openai.models.chat.completions.ChatCompletionMessageToolCall; @@ -38,7 +41,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -564,4 +569,482 @@ void toolCallsNoCaptureContent() { KeyValue.of("index", Value.of(0)), KeyValue.of("message", Value.of(emptyMap()))))); } + + @Test + void streamNoCaptureContent() { + OpenAIClient client = + OpenAITelemetry.builder(testing.getOpenTelemetry()).build().wrap(getRawClient()); + + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(Collections.singletonList(createUserMessage(TEST_CHAT_INPUT))) + .model(TEST_CHAT_MODEL) + .build(); + + List chunks; + try (StreamResponse result = + client.chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + String fullMessage = + chunks.stream() + .map( + cc -> { + if (cc.choices().isEmpty()) { + return Optional.empty(); + } + return cc.choices().get(0).delta().content(); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.joining()); + + String content = "Atlantic Ocean."; + assertThat(fullMessage).isEqualTo(content); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("stop")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(emptyMap())), + log -> { + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(0)), + KeyValue.of("message", Value.of(emptyMap())))); + }); + } + + @Test + void streamMultipleChoicesNoCaptureContent() { + OpenAIClient client = + OpenAITelemetry.builder(testing.getOpenTelemetry()).build().wrap(getRawClient()); + + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(Collections.singletonList(createUserMessage(TEST_CHAT_INPUT))) + .model(TEST_CHAT_MODEL) + .n(2) + .build(); + + List chunks; + try (StreamResponse result = + client.chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + StringBuilder content1Builder = new StringBuilder(); + StringBuilder content2Builder = new StringBuilder(); + for (ChatCompletionChunk chunk : chunks) { + if (chunk.choices().isEmpty()) { + continue; + } + ChatCompletionChunk.Choice choice = chunk.choices().get(0); + switch ((int) choice.index()) { + case 0: + content1Builder.append(choice.delta().content().orElse("")); + break; + case 1: + content2Builder.append(choice.delta().content().orElse("")); + break; + default: + // fallthrough + } + } + + String content1 = "Atlantic Ocean."; + assertThat(content1Builder.toString()).isEqualTo(content1); + String content2 = "South Atlantic Ocean."; + assertThat(content2Builder.toString()).isEqualTo(content2); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("stop", "stop")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(emptyMap())), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(0)), + KeyValue.of("message", Value.of(emptyMap())))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(1)), + KeyValue.of("message", Value.of(emptyMap()))))); + } + + @Test + void streamToolCallsNoCaptureContent() { + OpenAIClient client = + OpenAITelemetry.builder(testing.getOpenTelemetry()).build().wrap(getRawClient()); + + List chatMessages = new ArrayList<>(); + chatMessages.add(createSystemMessage("You are a helpful assistant providing weather updates.")); + chatMessages.add(createUserMessage("What is the weather in New York City and London?")); + + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(chatMessages) + .model(TEST_CHAT_MODEL) + .addTool(buildGetWeatherToolDefinition()) + .build(); + + List chunks; + try (StreamResponse result = + client.chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + List toolCalls = new ArrayList<>(); + + ChatCompletionMessageToolCall.Builder currentToolCall = null; + ChatCompletionMessageToolCall.Function.Builder currentFunction = null; + StringBuilder currentArgs = null; + + for (ChatCompletionChunk chunk : chunks) { + List calls = + chunk.choices().get(0).delta().toolCalls().orElse(emptyList()); + if (calls.isEmpty()) { + continue; + } + for (ChatCompletionChunk.Choice.Delta.ToolCall call : calls) { + if (call.id().isPresent()) { + if (currentToolCall != null) { + if (currentFunction != null && currentArgs != null) { + currentFunction.arguments(currentArgs.toString()); + currentToolCall.function(currentFunction.build()); + } + toolCalls.add(currentToolCall.build()); + } + currentToolCall = ChatCompletionMessageToolCall.builder().id(call.id().get()); + currentFunction = ChatCompletionMessageToolCall.Function.builder(); + currentArgs = new StringBuilder(); + } + if (call.function().isPresent()) { + if (call.function().get().name().isPresent()) { + if (currentFunction != null) { + currentFunction.name(call.function().get().name().get()); + } + } + if (call.function().get().arguments().isPresent()) { + if (currentArgs != null) { + currentArgs.append(call.function().get().arguments().get()); + } + } + } + } + } + if (currentToolCall != null) { + if (currentFunction != null && currentArgs != null) { + currentFunction.arguments(currentArgs.toString()); + currentToolCall.function(currentFunction.build()); + } + toolCalls.add(currentToolCall.build()); + } + + String newYorkCallId = + toolCalls.stream() + .filter(call -> call.function().arguments().contains("New York")) + .map(ChatCompletionMessageToolCall::id) + .findFirst() + .get(); + String londonCallId = + toolCalls.stream() + .filter(call -> call.function().arguments().contains("London")) + .map(ChatCompletionMessageToolCall::id) + .findFirst() + .get(); + + assertThat(newYorkCallId).startsWith("call_"); + assertThat(londonCallId).startsWith("call_"); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("tool_calls")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(EVENT_NAME, "gen_ai.system.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(emptyMap())), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(emptyMap())), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("tool_calls")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "message", + Value.of( + KeyValue.of( + "tool_calls", + Value.of( + Value.of( + KeyValue.of( + "function", + Value.of( + KeyValue.of( + "name", Value.of("get_weather")))), + KeyValue.of("id", Value.of(newYorkCallId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of( + "function", + Value.of( + KeyValue.of( + "name", Value.of("get_weather")))), + KeyValue.of("id", Value.of(londonCallId)), + KeyValue.of("type", Value.of("function")))))))))); + + getTesting().clearData(); + + ChatCompletionMessageParam assistantMessage = createAssistantMessage(toolCalls); + + chatMessages.add(assistantMessage); + chatMessages.add(createToolMessage("25 degrees and sunny", newYorkCallId)); + chatMessages.add(createToolMessage("15 degrees and raining", londonCallId)); + + params = + ChatCompletionCreateParams.builder() + .messages(chatMessages) + .model(TEST_CHAT_MODEL) + .addTool(buildGetWeatherToolDefinition()) + .build(); + + try (StreamResponse result = + client.chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("stop")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx1 = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(EVENT_NAME, "gen_ai.system.message")) + .hasSpanContext(spanCtx1) + .hasBody(Value.of(emptyMap())), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx1) + .hasBody(Value.of(emptyMap())), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(EVENT_NAME, "gen_ai.assistant.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of( + "tool_calls", + Value.of( + Value.of( + KeyValue.of( + "function", + Value.of(KeyValue.of("name", Value.of("get_weather")))), + KeyValue.of("id", Value.of(newYorkCallId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of( + "function", + Value.of(KeyValue.of("name", Value.of("get_weather")))), + KeyValue.of("id", Value.of(londonCallId)), + KeyValue.of("type", Value.of("function"))))))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody(Value.of(KeyValue.of("id", Value.of(newYorkCallId)))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody(Value.of(KeyValue.of("id", Value.of(londonCallId)))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(0)), + KeyValue.of("message", Value.of(emptyMap()))))); + } } diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java index 183152ffe904..036408c063ca 100644 --- a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java @@ -27,6 +27,7 @@ import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiSystemIncubatingValues.OPENAI; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiTokenTypeIncubatingValues.COMPLETION; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiTokenTypeIncubatingValues.INPUT; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -35,16 +36,19 @@ import com.openai.client.okhttp.OpenAIOkHttpClient; import com.openai.core.JsonObject; import com.openai.core.JsonValue; +import com.openai.core.http.StreamResponse; import com.openai.errors.OpenAIIoException; import com.openai.models.FunctionDefinition; import com.openai.models.FunctionParameters; import com.openai.models.ResponseFormatText; import com.openai.models.chat.completions.ChatCompletion; import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam; +import com.openai.models.chat.completions.ChatCompletionChunk; import com.openai.models.chat.completions.ChatCompletionCreateParams; import com.openai.models.chat.completions.ChatCompletionDeveloperMessageParam; import com.openai.models.chat.completions.ChatCompletionMessageParam; import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.chat.completions.ChatCompletionStreamOptions; import com.openai.models.chat.completions.ChatCompletionSystemMessageParam; import com.openai.models.chat.completions.ChatCompletionTool; import com.openai.models.chat.completions.ChatCompletionToolMessageParam; @@ -62,7 +66,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -859,6 +865,642 @@ void connectionError() { .hasBody(Value.of(KeyValue.of("content", Value.of(TEST_CHAT_INPUT))))); } + @Test + void stream() { + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(Collections.singletonList(createUserMessage(TEST_CHAT_INPUT))) + .model(TEST_CHAT_MODEL) + .build(); + + List chunks; + try (StreamResponse result = + getClient().chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + String fullMessage = + chunks.stream() + .map( + cc -> { + if (cc.choices().isEmpty()) { + return Optional.empty(); + } + return cc.choices().get(0).delta().content(); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.joining()); + + String content = "Atlantic Ocean."; + assertThat(fullMessage).isEqualTo(content); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("stop")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(KeyValue.of("content", Value.of(TEST_CHAT_INPUT)))), + log -> { + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "message", Value.of(KeyValue.of("content", Value.of(content)))))); + }); + } + + @Test + void streamIncludeUsage() { + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(Collections.singletonList(createUserMessage(TEST_CHAT_INPUT))) + .model(TEST_CHAT_MODEL) + .streamOptions(ChatCompletionStreamOptions.builder().includeUsage(true).build()) + .build(); + + List chunks; + try (StreamResponse result = + getClient().chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + String fullMessage = + chunks.stream() + .map( + cc -> { + if (cc.choices().isEmpty()) { + return Optional.empty(); + } + return cc.choices().get(0).delta().content(); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.joining()); + + String content = "South Atlantic Ocean"; + assertThat(fullMessage).isEqualTo(content); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("stop")), + equalTo(GEN_AI_USAGE_INPUT_TOKENS, 22L), + equalTo(GEN_AI_USAGE_OUTPUT_TOKENS, 3L))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL)))), + metric -> + metric + .hasName("gen_ai.client.token.usage") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(22.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + equalTo(GEN_AI_TOKEN_TYPE, INPUT)), + point -> + point + .hasSum(3.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + equalTo(GEN_AI_TOKEN_TYPE, COMPLETION))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(KeyValue.of("content", Value.of(TEST_CHAT_INPUT)))), + log -> { + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "message", Value.of(KeyValue.of("content", Value.of(content)))))); + }); + } + + @Test + void streamMultipleChoices() { + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(Collections.singletonList(createUserMessage(TEST_CHAT_INPUT))) + .model(TEST_CHAT_MODEL) + .n(2) + .build(); + + List chunks; + try (StreamResponse result = + getClient().chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + StringBuilder content1Builder = new StringBuilder(); + StringBuilder content2Builder = new StringBuilder(); + for (ChatCompletionChunk chunk : chunks) { + if (chunk.choices().isEmpty()) { + continue; + } + ChatCompletionChunk.Choice choice = chunk.choices().get(0); + switch ((int) choice.index()) { + case 0: + content1Builder.append(choice.delta().content().orElse("")); + break; + case 1: + content2Builder.append(choice.delta().content().orElse("")); + break; + default: + // fallthrough + } + } + + String content1 = "Atlantic Ocean."; + assertThat(content1Builder.toString()).isEqualTo(content1); + String content2 = "South Atlantic Ocean."; + assertThat(content2Builder.toString()).isEqualTo(content2); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("stop", "stop")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(KeyValue.of("content", Value.of(TEST_CHAT_INPUT)))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "message", Value.of(KeyValue.of("content", Value.of(content1)))))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(1)), + KeyValue.of( + "message", Value.of(KeyValue.of("content", Value.of(content2))))))); + } + + @Test + void streamToolCalls() { + List chatMessages = new ArrayList<>(); + chatMessages.add(createSystemMessage("You are a helpful assistant providing weather updates.")); + chatMessages.add(createUserMessage("What is the weather in New York City and London?")); + + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(chatMessages) + .model(TEST_CHAT_MODEL) + .addTool(buildGetWeatherToolDefinition()) + .build(); + + List chunks; + try (StreamResponse result = + getClient().chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + List toolCalls = new ArrayList<>(); + + ChatCompletionMessageToolCall.Builder currentToolCall = null; + ChatCompletionMessageToolCall.Function.Builder currentFunction = null; + StringBuilder currentArgs = null; + + for (ChatCompletionChunk chunk : chunks) { + List calls = + chunk.choices().get(0).delta().toolCalls().orElse(emptyList()); + if (calls.isEmpty()) { + continue; + } + for (ChatCompletionChunk.Choice.Delta.ToolCall call : calls) { + if (call.id().isPresent()) { + if (currentToolCall != null) { + currentFunction.arguments(currentArgs.toString()); + currentToolCall.function(currentFunction.build()); + toolCalls.add(currentToolCall.build()); + } + currentToolCall = ChatCompletionMessageToolCall.builder().id(call.id().get()); + currentFunction = ChatCompletionMessageToolCall.Function.builder(); + currentArgs = new StringBuilder(); + } + if (call.function().isPresent()) { + if (call.function().get().name().isPresent()) { + currentFunction.name(call.function().get().name().get()); + } + if (call.function().get().arguments().isPresent()) { + currentArgs.append(call.function().get().arguments().get()); + } + } + } + } + if (currentToolCall != null) { + currentFunction.arguments(currentArgs.toString()); + currentToolCall.function(currentFunction.build()); + toolCalls.add(currentToolCall.build()); + } + + String newYorkCallId = + toolCalls.stream() + .filter(call -> call.function().arguments().contains("New York")) + .map(ChatCompletionMessageToolCall::id) + .findFirst() + .get(); + String londonCallId = + toolCalls.stream() + .filter(call -> call.function().arguments().contains("London")) + .map(ChatCompletionMessageToolCall::id) + .findFirst() + .get(); + + assertThat(newYorkCallId).startsWith("call_"); + assertThat(londonCallId).startsWith("call_"); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("tool_calls")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(EVENT_NAME, "gen_ai.system.message")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of( + "content", + Value.of( + "You are a helpful assistant providing weather updates.")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of( + "content", + Value.of("What is the weather in New York City and London?")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("tool_calls")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "message", + Value.of( + KeyValue.of( + "tool_calls", + Value.of( + Value.of( + KeyValue.of( + "function", + Value.of( + KeyValue.of( + "name", Value.of("get_weather")), + KeyValue.of( + "arguments", + Value.of( + "{\"location\": \"New York City\"}")))), + KeyValue.of("id", Value.of(newYorkCallId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of( + "function", + Value.of( + KeyValue.of( + "name", Value.of("get_weather")), + KeyValue.of( + "arguments", + Value.of( + "{\"location\": \"London\"}")))), + KeyValue.of("id", Value.of(londonCallId)), + KeyValue.of("type", Value.of("function")))))))))); + + getTesting().clearData(); + + ChatCompletionMessageParam assistantMessage = createAssistantMessage(toolCalls); + + chatMessages.add(assistantMessage); + chatMessages.add(createToolMessage("25 degrees and sunny", newYorkCallId)); + chatMessages.add(createToolMessage("15 degrees and raining", londonCallId)); + + params = + ChatCompletionCreateParams.builder() + .messages(chatMessages) + .model(TEST_CHAT_MODEL) + .addTool(buildGetWeatherToolDefinition()) + .build(); + + try (StreamResponse result = + getClient().chat().completions().createStreaming(params)) { + chunks = result.stream().collect(Collectors.toList()); + } + + String finalAnswer = + chunks.stream() + .map( + cc -> { + if (cc.choices().isEmpty()) { + return Optional.empty(); + } + return cc.choices().get(0).delta().content(); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.joining()); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + satisfies(GEN_AI_RESPONSE_ID, id -> id.startsWith("chatcmpl-")), + equalTo(GEN_AI_RESPONSE_MODEL, TEST_CHAT_RESPONSE_MODEL), + satisfies( + GEN_AI_RESPONSE_FINISH_REASONS, + reasons -> reasons.containsExactly("stop")))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL), + equalTo( + GEN_AI_RESPONSE_MODEL, + TEST_CHAT_RESPONSE_MODEL))))); + + SpanContext spanCtx1 = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(EVENT_NAME, "gen_ai.system.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of( + "content", + Value.of( + "You are a helpful assistant providing weather updates.")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of( + "content", + Value.of("What is the weather in New York City and London?")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(EVENT_NAME, "gen_ai.assistant.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of( + "tool_calls", + Value.of( + Value.of( + KeyValue.of( + "function", + Value.of( + KeyValue.of("name", Value.of("get_weather")), + KeyValue.of( + "arguments", + Value.of( + "{\"location\": \"New York City\"}")))), + KeyValue.of("id", Value.of(newYorkCallId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of( + "function", + Value.of( + KeyValue.of("name", Value.of("get_weather")), + KeyValue.of( + "arguments", + Value.of("{\"location\": \"London\"}")))), + KeyValue.of("id", Value.of(londonCallId)), + KeyValue.of("type", Value.of("function"))))))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("id", Value.of(newYorkCallId)), + KeyValue.of("content", Value.of("25 degrees and sunny")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("id", Value.of(londonCallId)), + KeyValue.of("content", Value.of("15 degrees and raining")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("stop")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "message", + Value.of(KeyValue.of("content", Value.of(finalAnswer))))))); + } + protected static ChatCompletionMessageParam createUserMessage(String content) { return ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam.builder() diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.stream.yaml b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.stream.yaml new file mode 100644 index 000000000000..7c42684912ce --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.stream.yaml @@ -0,0 +1,59 @@ +--- +id: 70431a6b-8879-4ece-a010-cb14a507a091 +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "Answer in up to 3 words: Which ocean contains Bouvet Island?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuYarH1A2li9SaCpSoQpbD7VYp9eq","object":"chat.completion.chunk","created":1752819045,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuYarH1A2li9SaCpSoQpbD7VYp9eq","object":"chat.completion.chunk","created":1752819045,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Atlantic"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuYarH1A2li9SaCpSoQpbD7VYp9eq","object":"chat.completion.chunk","created":1752819045,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" Ocean"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuYarH1A2li9SaCpSoQpbD7VYp9eq","object":"chat.completion.chunk","created":1752819045,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuYarH1A2li9SaCpSoQpbD7VYp9eq","object":"chat.completion.chunk","created":1752819045,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 06:10:45 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "149" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "151" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999983" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_d5030fd2b49fb21828c03fc77c6b2ade + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 960fd154aca8e3a1-NRT + alt-svc: h3=":443"; ma=86400 +uuid: 70431a6b-8879-4ece-a010-cb14a507a091 +persistent: true +insertionIndex: 14 diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamincludeusage.yaml b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamincludeusage.yaml new file mode 100644 index 000000000000..55fd9b9aab19 --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamincludeusage.yaml @@ -0,0 +1,64 @@ +--- +id: 10de19b3-3332-49b4-924b-b5087c4726ae +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "Answer in up to 3 words: Which ocean contains Bouvet Island?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "stream_options" : { + "include_usage" : true + }, + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuYt76LLPdlwJfNjE13YCfKt4dKQG","object":"chat.completion.chunk","created":1752820177,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + + data: {"id":"chatcmpl-BuYt76LLPdlwJfNjE13YCfKt4dKQG","object":"chat.completion.chunk","created":1752820177,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"South"},"logprobs":null,"finish_reason":null}],"usage":null} + + data: {"id":"chatcmpl-BuYt76LLPdlwJfNjE13YCfKt4dKQG","object":"chat.completion.chunk","created":1752820177,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" Atlantic"},"logprobs":null,"finish_reason":null}],"usage":null} + + data: {"id":"chatcmpl-BuYt76LLPdlwJfNjE13YCfKt4dKQG","object":"chat.completion.chunk","created":1752820177,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" Ocean"},"logprobs":null,"finish_reason":null}],"usage":null} + + data: {"id":"chatcmpl-BuYt76LLPdlwJfNjE13YCfKt4dKQG","object":"chat.completion.chunk","created":1752820177,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + + data: {"id":"chatcmpl-BuYt76LLPdlwJfNjE13YCfKt4dKQG","object":"chat.completion.chunk","created":1752820177,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":22,"completion_tokens":3,"total_tokens":25,"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}}} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 06:29:37 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "265" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "268" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999982" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_43c78dbd24f6666bf6a60a13854cf611 + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 960fecfc3f95e375-NRT + alt-svc: h3=":443"; ma=86400 +uuid: 10de19b3-3332-49b4-924b-b5087c4726ae +persistent: true +insertionIndex: 16 diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streammultiplechoices.yaml b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streammultiplechoices.yaml new file mode 100644 index 000000000000..d8fca6b05fb5 --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streammultiplechoices.yaml @@ -0,0 +1,72 @@ +--- +id: fc0e38fc-c280-47f6-aeea-87f8d9e2af69 +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "Answer in up to 3 words: Which ocean contains Bouvet Island?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "n" : 2, + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Atlantic"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" Ocean"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":1,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"South"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":1,"delta":{"content":" Atlantic"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":1,"delta":{"content":" Ocean"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + + data: {"id":"chatcmpl-BuZ3sYIJd0JIQsI9OAOtndCPwDyfs","object":"chat.completion.chunk","created":1752820844,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":1,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 06:40:44 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "196" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "200" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999981" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_a65f112a04eeee3025bc5113ed662f71 + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 960ffd458db8684f-NRT + alt-svc: h3=":443"; ma=86400 +uuid: fc0e38fc-c280-47f6-aeea-87f8d9e2af69 +persistent: true +insertionIndex: 18 diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamtoolcalls.yaml b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamtoolcalls.yaml new file mode 100644 index 000000000000..4be8483a351d --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.openai.v1_1.abstractchattest.streamtoolcalls.yaml @@ -0,0 +1,856 @@ +--- +id: 3427d753-277b-437f-8ff5-790ab217e824 +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_GnmUduTX6kB6iIZNfBwGvbIr","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"N"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ew Y"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ork C"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ity\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_YK395pTD3ckjUarQFSEKo3ba","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"L"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ondo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZPwb2eH1iW90fJViyKoViFUokcp","object":"chat.completion.chunk","created":1752822212,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:03:33 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "1488" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "1492" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999970" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_93503d594fb2a68a7d4fdc4173ef23cf + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 96101ea24ad48a69-NRT + alt-svc: h3=":443"; ma=86400 +uuid: 3427d753-277b-437f-8ff5-790ab217e824 +persistent: true +insertionIndex: 20 +--- +id: 2c381a41-1922-4855-aeb1-b85a80b1db01 +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_BTWKIoPlMDKG8lfyWWQEL8ju","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"N"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ew Y"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ork C"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ity\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_knzR4w5FnJ32DzHvWJiryPAu","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"L"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ondo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZQf69dgaNqBz22ZphVOn57R319v","object":"chat.completion.chunk","created":1752822257,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:04:18 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "1146" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "1149" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999970" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_673228c68ca8f96d551cf58bc3c724ff + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 96101fbdfd11fd4b-NRT + alt-svc: h3=":443"; ma=86400 +uuid: 2c381a41-1922-4855-aeb1-b85a80b1db01 +persistent: true +insertionIndex: 22 +--- +id: 90aa1af8-45c4-49de-8e98-f5e85ad660cc +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_2dArSSG0pzdK6p6z4iuIHJhK","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"N"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ew Y"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ork C"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ity\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_CkLeq4RYxN1B2YwyEr3SpXR5","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"L"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ondo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZVzCgRkACYm4M1bg8ZHYHXpc8GG","object":"chat.completion.chunk","created":1752822587,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:09:49 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "1125" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "1128" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999970" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_861948c3ed4a61e8ee8b81b9ff113456 + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 961027cecadf62db-NRT + alt-svc: h3=":443"; ma=86400 +uuid: 90aa1af8-45c4-49de-8e98-f5e85ad660cc +persistent: true +insertionIndex: 24 +--- +id: 5fdcb25c-ede5-445d-b065-9949c1f039ce +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_FTt9aBx0oPfn1qCrPAU9pb1Q","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"N"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ew Y"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ork C"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ity\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_1P4L8GEXLJ3gipx0gYgvcqKw","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"L"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ondo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZWYC24HDALtxvAGDN5WmIhoBWpg","object":"chat.completion.chunk","created":1752822622,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:10:24 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "1277" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "1359" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999970" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_8b6ad1caa33731b3af25a6c5b2a51eda + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 961028ac1849687a-NRT + alt-svc: h3=":443"; ma=86400 +uuid: 5fdcb25c-ede5-445d-b065-9949c1f039ce +persistent: true +insertionIndex: 26 +--- +id: 965f9131-c36a-493a-b39a-eac81e34ea92 +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_ijuUaefQXjxEvImNyfCEEQKm","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"N"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ew Y"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ork C"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ity\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_ubZIJVKvsrYaLbQqmGe8vTSl","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"L"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ondo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXTIHqXtfM4WVqTR1SyuNk9xzS7","object":"chat.completion.chunk","created":1752822679,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:11:21 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "2898" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "2934" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999970" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_f98828ff20cfc44782019af059b105ea + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 96102a0e9b0380f5-NRT + alt-svc: h3=":443"; ma=86400 +uuid: 965f9131-c36a-493a-b39a-eac81e34ea92 +persistent: true +scenarioName: scenario-1-chat-completions +requiredScenarioState: Started +newScenarioState: scenario-1-chat-completions-2 +insertionIndex: 28 +--- +id: acac9ae3-1b22-4164-b5e7-201e78559c9a +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_Nt3jYXv6dAlBFeoVMaPRdd0J","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"N"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ew Y"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ork C"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ity\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_xYLuihId0mk2sXXAa7so3o98","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"L"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ondo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZXXacBXlI5dWOTuMnOa5wxvjGYw","object":"chat.completion.chunk","created":1752822683,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:11:24 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "1081" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "1086" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999970" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_2c2dc35e530969faaad52f5bfc8112ad + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 96102a28e8ebd79c-NRT + alt-svc: h3=":443"; ma=86400 +uuid: acac9ae3-1b22-4164-b5e7-201e78559c9a +persistent: true +scenarioName: scenario-1-chat-completions +requiredScenarioState: scenario-1-chat-completions-2 +insertionIndex: 29 +--- +id: a5ca08f1-cfa6-4ea6-abe0-d71379f5bbf1 +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_RqTAhsyrIFY4GqbN7mE0q0m8","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"N"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ew Y"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ork C"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ity\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_qwK9qR9CaijTcjyvtUfeSDPg","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"L"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ondo"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\"}"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYukxfokkVxGcFerk4Z6YBczfzA","object":"chat.completion.chunk","created":1752822768,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:12:49 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "968" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "970" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999970" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_99daaa10262c1681042b724e921965f8 + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 96102c366c81e38b-NRT + alt-svc: h3=":443"; ma=86400 +uuid: a5ca08f1-cfa6-4ea6-abe0-d71379f5bbf1 +persistent: true +insertionIndex: 32 +--- +id: d1c5e6b6-f13e-4ed8-985b-c80fd0258353 +name: chat_completions +request: + url: /chat/completions + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "content" : "You are a helpful assistant providing weather updates.", + "role" : "system" + }, { + "content" : "What is the weather in New York City and London?", + "role" : "user" + }, { + "role" : "assistant", + "tool_calls" : [ { + "id" : "call_RqTAhsyrIFY4GqbN7mE0q0m8", + "function" : { + "arguments" : "{\"location\": \"New York City\"}", + "name" : "get_weather" + }, + "type" : "function" + }, { + "id" : "call_qwK9qR9CaijTcjyvtUfeSDPg", + "function" : { + "arguments" : "{\"location\": \"London\"}", + "name" : "get_weather" + }, + "type" : "function" + } ] + }, { + "content" : "25 degrees and sunny", + "role" : "tool", + "tool_call_id" : "call_RqTAhsyrIFY4GqbN7mE0q0m8" + }, { + "content" : "15 degrees and raining", + "role" : "tool", + "tool_call_id" : "call_qwK9qR9CaijTcjyvtUfeSDPg" + } ], + "model" : "gpt-4o-mini", + "tools" : [ { + "function" : { + "name" : "get_weather", + "parameters" : { + "type" : "object", + "required" : [ "location" ], + "additionalProperties" : false, + "properties" : { + "location" : { + "description" : "The location to get the current temperature for", + "type" : "string" + } + } + } + }, + "type" : "function" + } ], + "stream" : true + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: |+ + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" current"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" follows"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":":\n\n"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"New"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" York"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" City"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"25"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" degrees"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" sunny"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"London"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"15"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" degrees"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" raining"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-BuZYwz5Xw1Ng0erMARmSuRUv4f7kr","object":"chat.completion.chunk","created":1752822770,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + + data: [DONE] + + headers: + Date: "Fri, 18 Jul 2025 07:12:50 GMT" + Content-Type: text/event-stream; charset=utf-8 + access-control-expose-headers: X-Request-ID + openai-organization: test_organization + openai-processing-ms: "250" + openai-project: proj_Pf1eM5R55Z35wBy4rt8PxAGq + openai-version: 2020-10-01 + x-envoy-upstream-service-time: "253" + x-ratelimit-limit-requests: "5000" + x-ratelimit-limit-tokens: "4000000" + x-ratelimit-remaining-requests: "4999" + x-ratelimit-remaining-tokens: "3999957" + x-ratelimit-reset-requests: 12ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_6cbded07d3aafa56a27eb1661b8a39e8 + strict-transport-security: max-age=31536000; includeSubDomains; preload + cf-cache-status: DYNAMIC + Set-Cookie: test_set_cookie + X-Content-Type-Options: nosniff + Server: cloudflare + CF-RAY: 96102c46594aec12-NRT + alt-svc: h3=":443"; ma=86400 +uuid: d1c5e6b6-f13e-4ed8-985b-c80fd0258353 +persistent: true +insertionIndex: 33 From b3a3e712e59682a8de397571896a6e54ef536a5e Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 18 Jul 2025 16:32:57 +0900 Subject: [PATCH 2/3] Add test for streaming connection error --- .../openai/v1_1/AbstractChatTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java index 036408c063ca..8e4a27f26ca2 100644 --- a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java @@ -1501,6 +1501,65 @@ void streamToolCalls() { Value.of(KeyValue.of("content", Value.of(finalAnswer))))))); } + @Test + void streamConnectionError() { + OpenAIClient client = + wrap( + OpenAIOkHttpClient.builder() + .baseUrl("http://localhost:9999/v5") + .apiKey("testing") + .maxRetries(0) + .build()); + + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(Collections.singletonList(createUserMessage(TEST_CHAT_INPUT))) + .model(TEST_CHAT_MODEL) + .build(); + + Throwable thrown = catchThrowable(() -> client.chat().completions().createStreaming(params)); + assertThat(thrown).isInstanceOf(OpenAIIoException.class); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + maybeWithTransportSpan( + span -> + span.hasException(thrown) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), + equalTo(GEN_AI_OPERATION_NAME, CHAT), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, "openai"), + equalTo(GEN_AI_OPERATION_NAME, "chat"), + equalTo(GEN_AI_REQUEST_MODEL, TEST_CHAT_MODEL))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, OPENAI), equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(KeyValue.of("content", Value.of(TEST_CHAT_INPUT))))); + } + protected static ChatCompletionMessageParam createUserMessage(String content) { return ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam.builder() From 39b2ee7b502aa62395dea679d19ba8196b40ce0e Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Sat, 19 Jul 2025 10:23:26 +0900 Subject: [PATCH 3/3] Fix visibility --- .../openai/v1_1/InstrumentedChatCompletionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java index 9c2e338efa52..a0d7b88af421 100644 --- a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java +++ b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/InstrumentedChatCompletionService.java @@ -100,7 +100,7 @@ private ChatCompletion createWithLogs( return result; } - public StreamResponse createStreaming( + private StreamResponse createStreaming( ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { Context parentCtx = Context.current(); if (!instrumenter.shouldStart(parentCtx, chatCompletionCreateParams)) {