diff --git a/genai_otel_conformance/specs.py b/genai_otel_conformance/specs.py index 6e0e9d2b..1dc6ad42 100644 --- a/genai_otel_conformance/specs.py +++ b/genai_otel_conformance/specs.py @@ -64,6 +64,7 @@ def attrs_for_requirement_level(self, level: RequirementLevel) -> tuple[str, ... "gen_ai.usage.cache_read.input_tokens", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", ] _INFERENCE_OPT_IN = [ "gen_ai.input.messages", diff --git a/tests/dotnet/azure-openai/Program.cs b/tests/dotnet/azure-openai/Program.cs index d6987cb1..6dc8ce4c 100644 --- a/tests/dotnet/azure-openai/Program.cs +++ b/tests/dotnet/azure-openai/Program.cs @@ -167,6 +167,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", completion.Usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", completion.Usage.OutputTokenCount); + if (completion.Usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", completion.Usage.OutputTokenDetails.ReasoningTokenCount); } var content = completion.Content[0].Text; @@ -191,6 +193,7 @@ static async Task RunPrototype() ["gen_ai.response.finish_reasons"] = new[] { completion.FinishReason.ToString() }, ["gen_ai.usage.input_tokens"] = completion.Usage?.InputTokenCount, ["gen_ai.usage.output_tokens"] = completion.Usage?.OutputTokenCount, + ["gen_ai.usage.reasoning.output_tokens"] = completion.Usage?.OutputTokenDetails?.ReasoningTokenCount > 0 ? completion.Usage.OutputTokenDetails.ReasoningTokenCount : null, ["gen_ai.input.messages"] = inputMessagesJson, ["gen_ai.output.messages"] = outputMessagesJson, ["server.address"] = endpoint.Host, @@ -228,6 +231,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", update.Usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", update.Usage.OutputTokenCount); + if (update.Usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", update.Usage.OutputTokenDetails.ReasoningTokenCount); } } @@ -301,6 +306,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", completion.Usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", completion.Usage.OutputTokenCount); + if (completion.Usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", completion.Usage.OutputTokenDetails.ReasoningTokenCount); } if (completion.ToolCalls.Count > 0) diff --git a/tests/dotnet/azure-openai/data-prototype.json b/tests/dotnet/azure-openai/data-prototype.json index 8e5e0c03..ee8863e3 100644 --- a/tests/dotnet/azure-openai/data-prototype.json +++ b/tests/dotnet/azure-openai/data-prototype.json @@ -10,6 +10,7 @@ "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", "server.address", "server.port" ] diff --git a/tests/dotnet/extensions-ai/Program.cs b/tests/dotnet/extensions-ai/Program.cs index 6e5b030d..19290f94 100644 --- a/tests/dotnet/extensions-ai/Program.cs +++ b/tests/dotnet/extensions-ai/Program.cs @@ -166,6 +166,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", completion.Usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", completion.Usage.OutputTokenCount); + if (completion.Usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", completion.Usage.OutputTokenDetails.ReasoningTokenCount); } var content = completion.Content[0].Text; @@ -190,6 +192,7 @@ static async Task RunPrototype() ["gen_ai.response.finish_reasons"] = new[] { completion.FinishReason.ToString() }, ["gen_ai.usage.input_tokens"] = completion.Usage?.InputTokenCount, ["gen_ai.usage.output_tokens"] = completion.Usage?.OutputTokenCount, + ["gen_ai.usage.reasoning.output_tokens"] = completion.Usage?.OutputTokenDetails?.ReasoningTokenCount > 0 ? completion.Usage.OutputTokenDetails.ReasoningTokenCount : null, ["gen_ai.input.messages"] = inputMessagesJson, ["gen_ai.output.messages"] = outputMessagesJson, ["server.address"] = endpoint.Host, @@ -227,6 +230,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", update.Usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", update.Usage.OutputTokenCount); + if (update.Usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", update.Usage.OutputTokenDetails.ReasoningTokenCount); } } @@ -300,6 +305,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", completion.Usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", completion.Usage.OutputTokenCount); + if (completion.Usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", completion.Usage.OutputTokenDetails.ReasoningTokenCount); } if (completion.ToolCalls.Count > 0) diff --git a/tests/dotnet/extensions-ai/data-prototype.json b/tests/dotnet/extensions-ai/data-prototype.json index 8e5e0c03..ee8863e3 100644 --- a/tests/dotnet/extensions-ai/data-prototype.json +++ b/tests/dotnet/extensions-ai/data-prototype.json @@ -10,6 +10,7 @@ "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", "server.address", "server.port" ] diff --git a/tests/dotnet/semantic-kernel/Program.cs b/tests/dotnet/semantic-kernel/Program.cs index 7b938777..2a1d1881 100644 --- a/tests/dotnet/semantic-kernel/Program.cs +++ b/tests/dotnet/semantic-kernel/Program.cs @@ -193,6 +193,7 @@ static async Task RunPrototype() string finishReason = null; int? inputTokens = null; int? outputTokens = null; + int? reasoningTokens = null; if (metadata != null) { if (metadata.TryGetValue("Id", out var id) && id != null) @@ -213,6 +214,11 @@ static async Task RunPrototype() outputTokens = usage.OutputTokenCount; activity?.SetTag("gen_ai.usage.input_tokens", inputTokens); activity?.SetTag("gen_ai.usage.output_tokens", outputTokens); + if (usage.OutputTokenDetails?.ReasoningTokenCount > 0) + { + reasoningTokens = usage.OutputTokenDetails.ReasoningTokenCount; + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", reasoningTokens); + } } } @@ -236,6 +242,7 @@ static async Task RunPrototype() ["gen_ai.response.finish_reasons"] = finishReason != null ? new[] { finishReason } : null, ["gen_ai.usage.input_tokens"] = inputTokens, ["gen_ai.usage.output_tokens"] = outputTokens, + ["gen_ai.usage.reasoning.output_tokens"] = reasoningTokens, ["gen_ai.input.messages"] = inputMessagesJson, ["gen_ai.output.messages"] = outputMessagesJson, ["server.address"] = endpoint.Host, @@ -279,6 +286,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", usage.OutputTokenCount); + if (usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", usage.OutputTokenDetails.ReasoningTokenCount); } } } @@ -347,6 +356,8 @@ static async Task RunPrototype() { activity?.SetTag("gen_ai.usage.input_tokens", usage.InputTokenCount); activity?.SetTag("gen_ai.usage.output_tokens", usage.OutputTokenCount); + if (usage.OutputTokenDetails?.ReasoningTokenCount > 0) + activity?.SetTag("gen_ai.usage.reasoning.output_tokens", usage.OutputTokenDetails.ReasoningTokenCount); } } diff --git a/tests/dotnet/semantic-kernel/data-prototype.json b/tests/dotnet/semantic-kernel/data-prototype.json index a26e5d2f..f42e38ac 100644 --- a/tests/dotnet/semantic-kernel/data-prototype.json +++ b/tests/dotnet/semantic-kernel/data-prototype.json @@ -10,6 +10,7 @@ "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", "server.address", "server.port" ], diff --git a/tests/java/openai/data-prototype.json b/tests/java/openai/data-prototype.json index fe4767e5..0c2b2515 100644 --- a/tests/java/openai/data-prototype.json +++ b/tests/java/openai/data-prototype.json @@ -9,7 +9,8 @@ "gen_ai.response.model", "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", - "gen_ai.usage.output_tokens" + "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens" ], "embeddings": [ "gen_ai.operation.name", diff --git a/tests/java/openai/src/main/java/com/example/openaitest/OpenAiPrototypeTest.java b/tests/java/openai/src/main/java/com/example/openaitest/OpenAiPrototypeTest.java index 80184029..3c452831 100644 --- a/tests/java/openai/src/main/java/com/example/openaitest/OpenAiPrototypeTest.java +++ b/tests/java/openai/src/main/java/com/example/openaitest/OpenAiPrototypeTest.java @@ -89,6 +89,10 @@ static void runChat(OpenAIClient client) { completion.usage().ifPresent(usage -> { span.setAttribute(longKey("gen_ai.usage.input_tokens"), usage.promptTokens()); span.setAttribute(longKey("gen_ai.usage.output_tokens"), usage.completionTokens()); + usage.completionTokensDetails().ifPresent(details -> { + details.reasoningTokens().filter(r -> r > 0).ifPresent(reasoning -> + span.setAttribute(longKey("gen_ai.usage.reasoning.output_tokens"), reasoning)); + }); }); String content = choice.message().content().orElse(""); @@ -132,6 +136,10 @@ static void runChat(OpenAIClient client) { logBuilder .setAttribute(longKey("gen_ai.usage.input_tokens"), usage.promptTokens()) .setAttribute(longKey("gen_ai.usage.output_tokens"), usage.completionTokens()); + usage.completionTokensDetails().ifPresent(details -> { + details.reasoningTokens().filter(r -> r > 0).ifPresent(reasoning -> + logBuilder.setAttribute(longKey("gen_ai.usage.reasoning.output_tokens"), reasoning)); + }); }); logBuilder.emit(); @@ -173,6 +181,10 @@ static void runChatStreaming(OpenAIClient client) { chunk.usage().ifPresent(usage -> { span.setAttribute(longKey("gen_ai.usage.input_tokens"), usage.promptTokens()); span.setAttribute(longKey("gen_ai.usage.output_tokens"), usage.completionTokens()); + usage.completionTokensDetails().ifPresent(details -> { + details.reasoningTokens().filter(r -> r > 0).ifPresent(reasoning -> + span.setAttribute(longKey("gen_ai.usage.reasoning.output_tokens"), reasoning)); + }); }); }); } catch (Exception e) { @@ -238,6 +250,10 @@ static void runChatToolCall(OpenAIClient client) { completion.usage().ifPresent(usage -> { span.setAttribute(longKey("gen_ai.usage.input_tokens"), usage.promptTokens()); span.setAttribute(longKey("gen_ai.usage.output_tokens"), usage.completionTokens()); + usage.completionTokensDetails().ifPresent(details -> { + details.reasoningTokens().filter(r -> r > 0).ifPresent(reasoning -> + span.setAttribute(longKey("gen_ai.usage.reasoning.output_tokens"), reasoning)); + }); }); List toolCalls = diff --git a/tests/js/azure-openai/data-prototype.json b/tests/js/azure-openai/data-prototype.json index 2ecfb5a3..bbabc72e 100644 --- a/tests/js/azure-openai/data-prototype.json +++ b/tests/js/azure-openai/data-prototype.json @@ -10,6 +10,7 @@ "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", "server.address", "server.port" ], diff --git a/tests/js/azure-openai/test_prototype.ts b/tests/js/azure-openai/test_prototype.ts index 507d4daf..75fd46f9 100644 --- a/tests/js/azure-openai/test_prototype.ts +++ b/tests/js/azure-openai/test_prototype.ts @@ -47,6 +47,9 @@ async function main() { if (resp.usage) { span.setAttribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens); span.setAttribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens); + if (resp.usage.completion_tokens_details?.reasoning_tokens) { + span.setAttribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens); + } } // Emit inference operation details event @@ -62,6 +65,7 @@ async function main() { "gen_ai.response.finish_reasons": resp.choices.map(c => c.finish_reason), "gen_ai.usage.input_tokens": resp.usage?.prompt_tokens, "gen_ai.usage.output_tokens": resp.usage?.completion_tokens, + "gen_ai.usage.reasoning.output_tokens": resp.usage?.completion_tokens_details?.reasoning_tokens || undefined, "gen_ai.input.messages": JSON.stringify( messages.map(m => ({ role: m.role, parts: [{ type: "text", content: m.content }] })) ), @@ -105,6 +109,7 @@ async function main() { let finishReason = ""; let inputTokens = 0; let outputTokens = 0; + let reasoningTokens: number | undefined; for await (const chunk of stream) { if (chunk.choices[0]?.delta?.content) { text += chunk.choices[0].delta.content; @@ -117,6 +122,9 @@ async function main() { if (chunk.usage) { inputTokens = chunk.usage.prompt_tokens; outputTokens = chunk.usage.completion_tokens; + if (chunk.usage.completion_tokens_details?.reasoning_tokens) { + reasoningTokens = chunk.usage.completion_tokens_details.reasoning_tokens; + } } } span.setAttribute("gen_ai.response.model", model); @@ -126,6 +134,7 @@ async function main() { } if (inputTokens) span.setAttribute("gen_ai.usage.input_tokens", inputTokens); if (outputTokens) span.setAttribute("gen_ai.usage.output_tokens", outputTokens); + if (reasoningTokens) span.setAttribute("gen_ai.usage.reasoning.output_tokens", reasoningTokens); console.log(` -> ${text.slice(0, 60)}`); span.end(); }); @@ -168,6 +177,9 @@ async function main() { if (resp.usage) { span.setAttribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens); span.setAttribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens); + if (resp.usage.completion_tokens_details?.reasoning_tokens) { + span.setAttribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens); + } } const toolCall = resp.choices[0].message.tool_calls?.[0]; if (toolCall?.type === "function") { diff --git a/tests/js/openai/data-prototype.json b/tests/js/openai/data-prototype.json index 2ecfb5a3..bbabc72e 100644 --- a/tests/js/openai/data-prototype.json +++ b/tests/js/openai/data-prototype.json @@ -10,6 +10,7 @@ "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", "server.address", "server.port" ], diff --git a/tests/js/openai/test_prototype.ts b/tests/js/openai/test_prototype.ts index ebc21e2f..d9de7d69 100644 --- a/tests/js/openai/test_prototype.ts +++ b/tests/js/openai/test_prototype.ts @@ -43,6 +43,9 @@ async function main() { if (resp.usage) { span.setAttribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens); span.setAttribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens); + if (resp.usage.completion_tokens_details?.reasoning_tokens) { + span.setAttribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens); + } } // Emit inference operation details event @@ -58,6 +61,7 @@ async function main() { "gen_ai.response.finish_reasons": resp.choices.map(c => c.finish_reason), "gen_ai.usage.input_tokens": resp.usage?.prompt_tokens, "gen_ai.usage.output_tokens": resp.usage?.completion_tokens, + "gen_ai.usage.reasoning.output_tokens": resp.usage?.completion_tokens_details?.reasoning_tokens || undefined, "gen_ai.input.messages": JSON.stringify( messages.map(m => ({ role: m.role, parts: [{ type: "text", content: m.content }] })) ), @@ -101,6 +105,7 @@ async function main() { let finishReason = ""; let inputTokens = 0; let outputTokens = 0; + let reasoningTokens: number | undefined; for await (const chunk of stream) { if (chunk.choices[0]?.delta?.content) { text += chunk.choices[0].delta.content; @@ -113,6 +118,9 @@ async function main() { if (chunk.usage) { inputTokens = chunk.usage.prompt_tokens; outputTokens = chunk.usage.completion_tokens; + if (chunk.usage.completion_tokens_details?.reasoning_tokens) { + reasoningTokens = chunk.usage.completion_tokens_details.reasoning_tokens; + } } } span.setAttribute("gen_ai.response.model", model); @@ -122,6 +130,7 @@ async function main() { } if (inputTokens) span.setAttribute("gen_ai.usage.input_tokens", inputTokens); if (outputTokens) span.setAttribute("gen_ai.usage.output_tokens", outputTokens); + if (reasoningTokens) span.setAttribute("gen_ai.usage.reasoning.output_tokens", reasoningTokens); console.log(` -> ${text.slice(0, 60)}`); span.end(); }); @@ -164,6 +173,9 @@ async function main() { if (resp.usage) { span.setAttribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens); span.setAttribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens); + if (resp.usage.completion_tokens_details?.reasoning_tokens) { + span.setAttribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens); + } } const toolCall = resp.choices[0].message.tool_calls?.[0]; if (toolCall?.type === "function") { diff --git a/tests/js/vertexai/data-prototype.json b/tests/js/vertexai/data-prototype.json index fbc2c8ff..1f6ebe52 100644 --- a/tests/js/vertexai/data-prototype.json +++ b/tests/js/vertexai/data-prototype.json @@ -8,7 +8,8 @@ "gen_ai.response.model", "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", - "gen_ai.usage.output_tokens" + "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens" ] }, "events": { diff --git a/tests/js/vertexai/test_prototype.ts b/tests/js/vertexai/test_prototype.ts index d3eaccc5..8a79f8cd 100644 --- a/tests/js/vertexai/test_prototype.ts +++ b/tests/js/vertexai/test_prototype.ts @@ -82,6 +82,9 @@ async function main() { if (resp.usageMetadata.candidatesTokenCount) { span.setAttribute("gen_ai.usage.output_tokens", resp.usageMetadata.candidatesTokenCount); } + if ((resp.usageMetadata as any).thoughtsTokenCount) { + span.setAttribute("gen_ai.usage.reasoning.output_tokens", (resp.usageMetadata as any).thoughtsTokenCount); + } } const text = candidate?.content?.parts?.[0]?.text ?? ""; @@ -97,6 +100,7 @@ async function main() { "gen_ai.response.finish_reasons": candidate?.finishReason ? [candidate.finishReason] : undefined, "gen_ai.usage.input_tokens": resp.usageMetadata?.promptTokenCount, "gen_ai.usage.output_tokens": resp.usageMetadata?.candidatesTokenCount, + "gen_ai.usage.reasoning.output_tokens": (resp.usageMetadata as any)?.thoughtsTokenCount || undefined, "gen_ai.input.messages": JSON.stringify([ { role: "user", parts: [{ type: "text", content: userMessage }] } ]), @@ -187,6 +191,9 @@ async function main() { if (aggregated.usageMetadata.candidatesTokenCount) { span.setAttribute("gen_ai.usage.output_tokens", aggregated.usageMetadata.candidatesTokenCount); } + if ((aggregated.usageMetadata as any).thoughtsTokenCount) { + span.setAttribute("gen_ai.usage.reasoning.output_tokens", (aggregated.usageMetadata as any).thoughtsTokenCount); + } } console.log(` -> ${text.slice(0, 60)}`); span.end(); diff --git a/tests/mock-server/mock_server/server.py b/tests/mock-server/mock_server/server.py index 57447546..849b9d19 100644 --- a/tests/mock-server/mock_server/server.py +++ b/tests/mock-server/mock_server/server.py @@ -36,6 +36,9 @@ "prompt_tokens": 25, "completion_tokens": 12, "total_tokens": 37, + "completion_tokens_details": { + "reasoning_tokens": 5, + }, }, } @@ -68,6 +71,9 @@ "prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70, + "completion_tokens_details": { + "reasoning_tokens": 8, + }, }, } @@ -217,6 +223,9 @@ def _stream_openai_chat(body): "prompt_tokens": 25, "completion_tokens": 6, "total_tokens": 31, + "completion_tokens_details": { + "reasoning_tokens": 2, + }, }, } ) @@ -430,6 +439,7 @@ def anthropic_messages(): "promptTokenCount": 25, "candidatesTokenCount": 12, "totalTokenCount": 37, + "thoughtsTokenCount": 5, }, "modelVersion": "gemini-2.0-flash", } @@ -461,6 +471,7 @@ def _stream_google_genai(): "promptTokenCount": 25, "candidatesTokenCount": 6, "totalTokenCount": 31, + "thoughtsTokenCount": 2, }, } yield json.dumps(final) + "\n" @@ -515,6 +526,7 @@ def _google_genai_stream_chunks(): "promptTokenCount": 25, "candidatesTokenCount": 6, "totalTokenCount": 31, + "thoughtsTokenCount": 2, }, }) return chunks diff --git a/tests/python/azure-openai/data-prototype.json b/tests/python/azure-openai/data-prototype.json index 69447190..7ab5f31f 100644 --- a/tests/python/azure-openai/data-prototype.json +++ b/tests/python/azure-openai/data-prototype.json @@ -10,6 +10,7 @@ "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", "server.address", "server.port" ], diff --git a/tests/python/azure-openai/test_prototype.py b/tests/python/azure-openai/test_prototype.py index 9882a95f..e160f0a7 100644 --- a/tests/python/azure-openai/test_prototype.py +++ b/tests/python/azure-openai/test_prototype.py @@ -38,6 +38,9 @@ def run_chat_prototype(client): if resp.usage: span.set_attribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens) span.set_attribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens) + if hasattr(resp.usage, "completion_tokens_details") and resp.usage.completion_tokens_details: + if getattr(resp.usage.completion_tokens_details, "reasoning_tokens", None): + span.set_attribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens) # Emit inference operation details event event_attrs = { @@ -62,6 +65,9 @@ def run_chat_prototype(client): if resp.usage: event_attrs["gen_ai.usage.input_tokens"] = resp.usage.prompt_tokens event_attrs["gen_ai.usage.output_tokens"] = resp.usage.completion_tokens + if hasattr(resp.usage, "completion_tokens_details") and resp.usage.completion_tokens_details: + if getattr(resp.usage.completion_tokens_details, "reasoning_tokens", None): + event_attrs["gen_ai.usage.reasoning.output_tokens"] = resp.usage.completion_tokens_details.reasoning_tokens if endpoint.hostname: event_attrs["server.address"] = endpoint.hostname if endpoint.port is not None: @@ -100,6 +106,7 @@ def run_chat_streaming_prototype(client): finish_reasons = [] input_tokens = None output_tokens = None + reasoning_tokens = None for chunk in stream: model = model or getattr(chunk, "model", None) response_id = response_id or getattr(chunk, "id", None) @@ -110,6 +117,9 @@ def run_chat_streaming_prototype(client): if chunk.usage: input_tokens = chunk.usage.prompt_tokens output_tokens = chunk.usage.completion_tokens + if hasattr(chunk.usage, "completion_tokens_details") and chunk.usage.completion_tokens_details: + if getattr(chunk.usage.completion_tokens_details, "reasoning_tokens", None): + reasoning_tokens = chunk.usage.completion_tokens_details.reasoning_tokens if model: span.set_attribute("gen_ai.response.model", model) if response_id: @@ -120,6 +130,8 @@ def run_chat_streaming_prototype(client): span.set_attribute("gen_ai.usage.input_tokens", input_tokens) if output_tokens is not None: span.set_attribute("gen_ai.usage.output_tokens", output_tokens) + if reasoning_tokens: + span.set_attribute("gen_ai.usage.reasoning.output_tokens", reasoning_tokens) print(f" -> {text[:60]}") @@ -163,6 +175,9 @@ def run_chat_tool_call_prototype(client): if resp.usage: span.set_attribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens) span.set_attribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens) + if hasattr(resp.usage, "completion_tokens_details") and resp.usage.completion_tokens_details: + if getattr(resp.usage.completion_tokens_details, "reasoning_tokens", None): + span.set_attribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens) choice = resp.choices[0] if choice.message.tool_calls: print(f" -> tool_call: {choice.message.tool_calls[0].function.name}") diff --git a/tests/python/google-adk/data-prototype.json b/tests/python/google-adk/data-prototype.json index 9e6b8431..9b64cf0a 100644 --- a/tests/python/google-adk/data-prototype.json +++ b/tests/python/google-adk/data-prototype.json @@ -8,7 +8,8 @@ "gen_ai.response.finish_reasons", "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", - "gen_ai.usage.output_tokens" + "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens" ] } } diff --git a/tests/python/google-adk/test_prototype.py b/tests/python/google-adk/test_prototype.py index 3b2e02d4..4f837be3 100644 --- a/tests/python/google-adk/test_prototype.py +++ b/tests/python/google-adk/test_prototype.py @@ -147,13 +147,17 @@ async def _run(): if usage_metadata is not None: prompt_token_count = getattr(usage_metadata, "prompt_token_count", None) candidate_token_count = getattr(usage_metadata, "candidates_token_count", None) + thoughts_token_count = getattr(usage_metadata, "thoughts_token_count", None) if isinstance(usage_metadata, dict): prompt_token_count = usage_metadata.get("prompt_token_count") candidate_token_count = usage_metadata.get("candidates_token_count") + thoughts_token_count = usage_metadata.get("thoughts_token_count") if prompt_token_count is not None: span.set_attribute("gen_ai.usage.input_tokens", prompt_token_count) if candidate_token_count is not None: span.set_attribute("gen_ai.usage.output_tokens", candidate_token_count) + if thoughts_token_count: + span.set_attribute("gen_ai.usage.reasoning.output_tokens", thoughts_token_count) if finish_reason is not None: span.set_attribute( "gen_ai.response.finish_reasons", diff --git a/tests/python/google-genai/data-prototype.json b/tests/python/google-genai/data-prototype.json index 9a34ebb5..7c20574b 100644 --- a/tests/python/google-genai/data-prototype.json +++ b/tests/python/google-genai/data-prototype.json @@ -8,7 +8,8 @@ "gen_ai.response.model", "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", - "gen_ai.usage.output_tokens" + "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens" ], "embeddings": [ "gen_ai.operation.name", diff --git a/tests/python/google-genai/test_prototype.py b/tests/python/google-genai/test_prototype.py index 1b1deda3..75dfb168 100644 --- a/tests/python/google-genai/test_prototype.py +++ b/tests/python/google-genai/test_prototype.py @@ -48,6 +48,8 @@ def run_chat(): span.set_attribute("gen_ai.usage.input_tokens", response.usage_metadata.prompt_token_count) if hasattr(response.usage_metadata, "candidates_token_count") and response.usage_metadata.candidates_token_count: span.set_attribute("gen_ai.usage.output_tokens", response.usage_metadata.candidates_token_count) + if hasattr(response.usage_metadata, "thoughts_token_count") and response.usage_metadata.thoughts_token_count: + span.set_attribute("gen_ai.usage.reasoning.output_tokens", response.usage_metadata.thoughts_token_count) # Emit inference operation details event event_attrs = { @@ -73,6 +75,8 @@ def run_chat(): event_attrs["gen_ai.usage.input_tokens"] = response.usage_metadata.prompt_token_count if hasattr(response.usage_metadata, "candidates_token_count") and response.usage_metadata.candidates_token_count: event_attrs["gen_ai.usage.output_tokens"] = response.usage_metadata.candidates_token_count + if hasattr(response.usage_metadata, "thoughts_token_count") and response.usage_metadata.thoughts_token_count: + event_attrs["gen_ai.usage.reasoning.output_tokens"] = response.usage_metadata.thoughts_token_count get_logger_provider().get_logger("gen_ai.prototype").emit( event_name="gen_ai.client.inference.operation.details", body="Inference operation details", @@ -187,6 +191,8 @@ def run_chat_streaming(): span.set_attribute("gen_ai.usage.input_tokens", last_chunk.usage_metadata.prompt_token_count) if hasattr(last_chunk.usage_metadata, "candidates_token_count") and last_chunk.usage_metadata.candidates_token_count: span.set_attribute("gen_ai.usage.output_tokens", last_chunk.usage_metadata.candidates_token_count) + if hasattr(last_chunk.usage_metadata, "thoughts_token_count") and last_chunk.usage_metadata.thoughts_token_count: + span.set_attribute("gen_ai.usage.reasoning.output_tokens", last_chunk.usage_metadata.thoughts_token_count) print(f" -> {text[:60]}") diff --git a/tests/python/openai/data-prototype.json b/tests/python/openai/data-prototype.json index 69447190..7ab5f31f 100644 --- a/tests/python/openai/data-prototype.json +++ b/tests/python/openai/data-prototype.json @@ -10,6 +10,7 @@ "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens", "server.address", "server.port" ], diff --git a/tests/python/openai/test_prototype.py b/tests/python/openai/test_prototype.py index e7be94b5..4b3be295 100644 --- a/tests/python/openai/test_prototype.py +++ b/tests/python/openai/test_prototype.py @@ -38,6 +38,9 @@ def run_chat_prototype(client): if resp.usage: span.set_attribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens) span.set_attribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens) + if hasattr(resp.usage, "completion_tokens_details") and resp.usage.completion_tokens_details: + if getattr(resp.usage.completion_tokens_details, "reasoning_tokens", None): + span.set_attribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens) # Emit inference operation details event event_attrs = { @@ -62,6 +65,9 @@ def run_chat_prototype(client): if resp.usage: event_attrs["gen_ai.usage.input_tokens"] = resp.usage.prompt_tokens event_attrs["gen_ai.usage.output_tokens"] = resp.usage.completion_tokens + if hasattr(resp.usage, "completion_tokens_details") and resp.usage.completion_tokens_details: + if getattr(resp.usage.completion_tokens_details, "reasoning_tokens", None): + event_attrs["gen_ai.usage.reasoning.output_tokens"] = resp.usage.completion_tokens_details.reasoning_tokens if endpoint.hostname: event_attrs["server.address"] = endpoint.hostname if endpoint.port is not None: @@ -100,6 +106,7 @@ def run_chat_streaming_prototype(client): finish_reasons = [] input_tokens = None output_tokens = None + reasoning_tokens = None for chunk in stream: model = model or getattr(chunk, "model", None) response_id = response_id or getattr(chunk, "id", None) @@ -110,6 +117,9 @@ def run_chat_streaming_prototype(client): if chunk.usage: input_tokens = chunk.usage.prompt_tokens output_tokens = chunk.usage.completion_tokens + if hasattr(chunk.usage, "completion_tokens_details") and chunk.usage.completion_tokens_details: + if getattr(chunk.usage.completion_tokens_details, "reasoning_tokens", None): + reasoning_tokens = chunk.usage.completion_tokens_details.reasoning_tokens if model: span.set_attribute("gen_ai.response.model", model) if response_id: @@ -120,6 +130,8 @@ def run_chat_streaming_prototype(client): span.set_attribute("gen_ai.usage.input_tokens", input_tokens) if output_tokens is not None: span.set_attribute("gen_ai.usage.output_tokens", output_tokens) + if reasoning_tokens: + span.set_attribute("gen_ai.usage.reasoning.output_tokens", reasoning_tokens) print(f" -> {text[:60]}") @@ -163,6 +175,9 @@ def run_chat_tool_call_prototype(client): if resp.usage: span.set_attribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens) span.set_attribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens) + if hasattr(resp.usage, "completion_tokens_details") and resp.usage.completion_tokens_details: + if getattr(resp.usage.completion_tokens_details, "reasoning_tokens", None): + span.set_attribute("gen_ai.usage.reasoning.output_tokens", resp.usage.completion_tokens_details.reasoning_tokens) choice = resp.choices[0] if choice.message.tool_calls: print(f" -> tool_call: {choice.message.tool_calls[0].function.name}") diff --git a/tests/python/vertexai/data-prototype.json b/tests/python/vertexai/data-prototype.json index fbc2c8ff..1f6ebe52 100644 --- a/tests/python/vertexai/data-prototype.json +++ b/tests/python/vertexai/data-prototype.json @@ -8,7 +8,8 @@ "gen_ai.response.model", "gen_ai.tool.definitions", "gen_ai.usage.input_tokens", - "gen_ai.usage.output_tokens" + "gen_ai.usage.output_tokens", + "gen_ai.usage.reasoning.output_tokens" ] }, "events": { diff --git a/tests/python/vertexai/test_prototype.py b/tests/python/vertexai/test_prototype.py index 47cbd8e6..25faa64b 100644 --- a/tests/python/vertexai/test_prototype.py +++ b/tests/python/vertexai/test_prototype.py @@ -80,6 +80,8 @@ def run_chat(): span.set_attribute("gen_ai.usage.input_tokens", response.usage_metadata.prompt_token_count) if response.usage_metadata.candidates_token_count: span.set_attribute("gen_ai.usage.output_tokens", response.usage_metadata.candidates_token_count) + if hasattr(response.usage_metadata, "thoughts_token_count") and response.usage_metadata.thoughts_token_count: + span.set_attribute("gen_ai.usage.reasoning.output_tokens", response.usage_metadata.thoughts_token_count) # Emit inference operation details event event_attrs = { @@ -104,6 +106,8 @@ def run_chat(): event_attrs["gen_ai.usage.input_tokens"] = response.usage_metadata.prompt_token_count if response.usage_metadata.candidates_token_count: event_attrs["gen_ai.usage.output_tokens"] = response.usage_metadata.candidates_token_count + if hasattr(response.usage_metadata, "thoughts_token_count") and response.usage_metadata.thoughts_token_count: + event_attrs["gen_ai.usage.reasoning.output_tokens"] = response.usage_metadata.thoughts_token_count get_logger_provider().get_logger("gen_ai.prototype").emit( event_name="gen_ai.client.inference.operation.details", body="Inference operation details", @@ -207,6 +211,8 @@ def run_chat_streaming(): span.set_attribute("gen_ai.usage.input_tokens", last_chunk.usage_metadata.prompt_token_count) if last_chunk.usage_metadata.candidates_token_count: span.set_attribute("gen_ai.usage.output_tokens", last_chunk.usage_metadata.candidates_token_count) + if hasattr(last_chunk.usage_metadata, "thoughts_token_count") and last_chunk.usage_metadata.thoughts_token_count: + span.set_attribute("gen_ai.usage.reasoning.output_tokens", last_chunk.usage_metadata.thoughts_token_count) print(f" -> {text[:60]}")