diff --git a/.gitignore b/.gitignore index 2fc81af..f7637f8 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ examples/internal/email-evals/email-evals /dist # Added by goreleaser init: dist/ + +# emacs +*~ diff --git a/Makefile b/Makefile index 5e8ccfd..e3d129d 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,6 @@ help: @echo " test-vcr-record - Record/update VCR cassettes (requires API keys)" @echo " test-vcr-verify - Verify VCR cassettes work without API keys" @echo " cover - Run tests with coverage report" - @echo " cover-path - Run coverage for specific path (e.g., make cover-path PATH=./config)" @echo " clean - Clean build artifacts and coverage files" @echo " fmt - Format Go code" @echo " lint - Run golangci-lint" diff --git a/examples/openai/main.go b/examples/openai/main.go index 46f2102..2ab8abb 100644 --- a/examples/openai/main.go +++ b/examples/openai/main.go @@ -8,6 +8,7 @@ import ( "github.com/openai/openai-go" "github.com/openai/openai-go/option" + "github.com/openai/openai-go/responses" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/trace" @@ -39,17 +40,26 @@ func main() { ctx, span := tracer.Start(context.Background(), "examples/openai/main.go") defer span.End() - // Make a simple chat completion request - resp, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{ - Messages: []openai.ChatCompletionMessageParamUnion{ - openai.UserMessage("What is the capital of France?"), - }, + // Make a simple Responses API request + resp, err := client.Responses.New(ctx, responses.ResponseNewParams{ Model: openai.ChatModelGPT4oMini, + Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("What is the capital of France?")}, }) if err != nil { log.Fatal(err) } - - fmt.Printf("Response: %s\n", resp.Choices[0].Message.Content) - fmt.Printf("View trace: %s\n", bt.Permalink(span)) + switch resp.Status { + case responses.ResponseStatusCompleted: + fmt.Printf("Response: %s\n", resp.OutputText()) + fmt.Printf("View trace: %s\n", bt.Permalink(span)) + case responses.ResponseStatusIncomplete: + fmt.Println("incomplete:", resp.IncompleteDetails.Reason) + fmt.Printf("Response: %s\n", resp.OutputText()) + fmt.Printf("View trace: %s\n", bt.Permalink(span)) + case responses.ResponseStatusFailed: + fmt.Println("failed:", resp.Error.Message) + fmt.Printf("View trace: %s\n", bt.Permalink(span)) + default: + fmt.Println("status:", resp.Status) + } } diff --git a/trace/contrib/openai/responses.go b/trace/contrib/openai/responses.go index 79ffb65..e3d1780 100644 --- a/trace/contrib/openai/responses.go +++ b/trace/contrib/openai/responses.go @@ -130,9 +130,8 @@ func (rt *responsesTracer) parseStreamingResponse(span trace.Span, body io.Reade } if msgType, ok := envelope["type"].(string); ok { - // the response.completed message has everything, so just parse that. Should we - // parse the other messages too? - if msgType == "response.completed" { + switch msgType { + case "response.completed", "response.failed", "response.incomplete": if msg, ok := envelope["response"].(map[string]any); ok { // For streaming responses, copy extra fields from the envelope // that might be present in the outer wrapper @@ -181,6 +180,7 @@ func (rt *responsesTracer) handleResponseCompletedMessage(span trace.Span, rawMs metadataFields := []string{ "id", "object", + "status", "system_fingerprint", "completion_tokens", "created", @@ -193,6 +193,9 @@ func (rt *responsesTracer) handleResponseCompletedMessage(span trace.Span, rawMs "content_filter_results", "reasoning", "text", + "usage", + "incomplete_details", + "error", } for _, field := range metadataFields { diff --git a/trace/contrib/openai/testdata/cassettes/TestOpenAIResponsesCompletedUsage.yaml b/trace/contrib/openai/testdata/cassettes/TestOpenAIResponsesCompletedUsage.yaml new file mode 100644 index 0000000..f62d8a4 --- /dev/null +++ b/trace/contrib/openai/testdata/cassettes/TestOpenAIResponsesCompletedUsage.yaml @@ -0,0 +1,164 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 64 + transfer_encoding: [] + trailer: {} + host: "" + remote_addr: "" + request_uri: "" + body: '{"input":"What is the capital of France?","model":"gpt-4o-mini"}' + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - OpenAI/Go 1.12.0 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - go + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.12.0 + X-Stainless-Retry-Count: + - "0" + X-Stainless-Runtime: + - go + X-Stainless-Runtime-Version: + - go1.26.2 + url: https://api.openai.com/v1/responses + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: true + body: |- + { + "id": "resp_00297490fcbc5dcc0069f0d13e07b0819ba4cb70853f95cc0e", + "object": "response", + "created_at": 1777389886, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1777389887, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "output": [ + { + "id": "msg_00297490fcbc5dcc0069f0d13f0a20819bb1002795595779e0", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The capital of France is Paris." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 14, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 8, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 22 + }, + "user": null, + "metadata": {} + } + headers: + Alt-Svc: + - h3=":443"; ma=86400 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 9f3713632b7b420b-EWR + Content-Type: + - application/json + Date: + - Tue, 28 Apr 2026 15:24:47 GMT + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - "1247" + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - "2020-10-01" + Server: + - cloudflare + Set-Cookie: + - __cf_bm=YyPcSpHIhDzxiZr_PPUz5BpglZaiN47o3bMWhvYrpQs-1777389885.94245-1.0.1.1-4pWcI.VMIhN1F7QL39HKPE2Vg2TBw7NY0KIK3C9oPVT8NPDyyLntvKFNnKngMo1zyBUN73JKAALmHagsaNm6ZNX5En.2r.Ce5xktre0looqN_4t5T3rQnqs5M2ZKI78t; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Tue, 28 Apr 2026 15:54:47 GMT + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + X-Ratelimit-Limit-Requests: + - "30000" + X-Ratelimit-Limit-Tokens: + - "150000000" + X-Ratelimit-Remaining-Requests: + - "29999" + X-Ratelimit-Remaining-Tokens: + - "149999967" + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_388462f6d6934e98b2adb25f5adaee39 + status: 200 OK + code: 200 + duration: 1.61271275s diff --git a/trace/contrib/openai/testdata/cassettes/TestOpenAIResponsesStreamingIncomplete.yaml b/trace/contrib/openai/testdata/cassettes/TestOpenAIResponsesStreamingIncomplete.yaml new file mode 100644 index 0000000..ad045e9 --- /dev/null +++ b/trace/contrib/openai/testdata/cassettes/TestOpenAIResponsesStreamingIncomplete.yaml @@ -0,0 +1,152 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 116 + transfer_encoding: [] + trailer: {} + host: "" + remote_addr: "" + request_uri: "" + body: '{"max_output_tokens":16,"input":"What is the history of the capital of France?","model":"gpt-4o-mini","stream":true}' + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - OpenAI/Go 1.12.0 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - go + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.12.0 + X-Stainless-Retry-Count: + - "0" + X-Stainless-Runtime: + - go + X-Stainless-Runtime-Version: + - go1.26.2 + url: https://api.openai.com/v1/responses + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_029e855511ca05f10069f0d0b0bdac819b9cdbe52098b831ab","object":"response","created_at":1777389744,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":16,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_029e855511ca05f10069f0d0b0bdac819b9cdbe52098b831ab","object":"response","created_at":1777389744,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":16,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"The","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"9PUOj9CiuEZZ9","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" history","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"7DlE81sQ","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"qYhvLaMtc0Qia","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" Paris","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"vNylhxPzYt","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"a3moehiA1qGzWeg","output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"dQWvVleLwHaS","output_index":0,"sequence_number":9} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" capital","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"gK8ZYDvM","output_index":0,"sequence_number":10} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"z81KHjUogBdQe","output_index":0,"sequence_number":11} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" France","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"3afkfJiDT","output_index":0,"sequence_number":12} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"7eLb1pSCShxfq8m","output_index":0,"sequence_number":13} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"BF3QxR2QVk34n","output_index":0,"sequence_number":14} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" rich","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"fF2RH06PpVV","output_index":0,"sequence_number":15} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" and","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"Z78UlFSmbg9R","output_index":0,"sequence_number":16} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" complex","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"nwGWIP4a","output_index":0,"sequence_number":17} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"ydgoVQ2CLddPf7C","output_index":0,"sequence_number":18} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" spanning","item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"obfuscation":"9wiCHZB","output_index":0,"sequence_number":19} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","logprobs":[],"output_index":0,"sequence_number":20,"text":"The history of Paris, the capital of France, is rich and complex, spanning"} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The history of Paris, the capital of France, is rich and complex, spanning"},"sequence_number":21} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The history of Paris, the capital of France, is rich and complex, spanning"}],"role":"assistant"},"output_index":0,"sequence_number":22} + + event: response.incomplete + data: {"type":"response.incomplete","response":{"id":"resp_029e855511ca05f10069f0d0b0bdac819b9cdbe52098b831ab","object":"response","created_at":1777389744,"status":"incomplete","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":16,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_029e855511ca05f10069f0d0b1b9a4819b844dc1f66e33f58b","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The history of Paris, the capital of France, is rich and complex, spanning"}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":17,"input_tokens_details":{"cached_tokens":0},"output_tokens":16,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":33},"user":null,"metadata":{}},"sequence_number":23} + + headers: + Alt-Svc: + - h3=":443"; ma=86400 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 9f370fef1bf2b785-EWR + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Tue, 28 Apr 2026 15:22:25 GMT + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - "294" + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - "2020-10-01" + Server: + - cloudflare + Set-Cookie: + - __cf_bm=vE_aZWmG4Or4IfRqUpKCgtK1eIFNpHTNSawzooHnEXc-1777389744.494582-1.0.1.1-8ijD1NUw6im6e6XvLKFICw7VACQltpl6carTRCf7NpV2E9kGQDcG_aTPe2JEQx8vxPyNatMXPtI.CrE0AwwLou_GsYqmfOdxfnAg_05ZdbByXzd1ZqGRHsfxJuTBNs8B; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Tue, 28 Apr 2026 15:52:25 GMT + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + X-Request-Id: + - req_46a1f6b19530490e85425745aa2c3078 + status: 200 OK + code: 200 + duration: 729.641209ms diff --git a/trace/contrib/openai/traceopenai_test.go b/trace/contrib/openai/traceopenai_test.go index 9470d08..dec2335 100644 --- a/trace/contrib/openai/traceopenai_test.go +++ b/trace/contrib/openai/traceopenai_test.go @@ -262,6 +262,88 @@ func TestOpenAIResponsesStreaming(t *testing.T) { assert.GreaterOrEqual(ttft, 0.0, "time_to_first_token should be >= 0") } +// TestOpenAIResponsesCompletedUsage verifies that a completed response records +// status "completed" and usage (input/output/total tokens) in metadata. +func TestOpenAIResponsesCompletedUsage(t *testing.T) { + client, _, exporter := setUpTest(t) + assert := assert.New(t) + require := require.New(t) + + resp, err := client.Responses.New(context.Background(), responses.ResponseNewParams{ + Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("What is the capital of France?")}, + Model: openai.ChatModelGPT4oMini, + }) + require.NoError(err) + assert.Contains(resp.OutputText(), "Paris") + + ts := exporter.FlushOne() + + metadata := ts.Metadata() + assert.Equal("completed", metadata["status"]) + + usage, ok := metadata["usage"].(map[string]interface{}) + require.True(ok, "usage should be present in metadata") + assert.Greater(usage["input_tokens"].(float64), float64(0)) + assert.Greater(usage["output_tokens"].(float64), float64(0)) + assert.Greater(usage["total_tokens"].(float64), float64(0)) + + metrics := ts.Metrics() + assert.Greater(metrics["tokens"], float64(0)) + assert.Greater(metrics["prompt_tokens"], float64(0)) + assert.Greater(metrics["completion_tokens"], float64(0)) +} + +// TestOpenAIResponsesStreamingIncomplete verifies that a streaming response truncated +// by max_output_tokens is recorded with status "incomplete" and usage in metadata. +func TestOpenAIResponsesStreamingIncomplete(t *testing.T) { + client, _, exporter := setUpTest(t) + assert := assert.New(t) + require := require.New(t) + + ctx := context.Background() + + timer := oteltest.NewTimer() + stream := client.Responses.NewStreaming(ctx, responses.ResponseNewParams{ + Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("What is the history of the capital of France?")}, + Model: openai.ChatModelGPT4oMini, + MaxOutputTokens: openai.Int(16), + }) + + var partialText string + for stream.Next() { + data := stream.Current() + if data.Text != "" { + partialText = data.Text + } + } + require.NoError(stream.Err()) + timeRange := timer.Tick() + + ts := exporter.FlushOne() + + ts.AssertInTimeRange(timeRange) + ts.AssertNameIs("openai.responses.create") + + assert.NotEmpty(partialText, "should have received partial output") + + metadata := ts.Metadata() + assert.Equal("incomplete", metadata["status"]) + + usage, ok := metadata["usage"].(map[string]interface{}) + require.True(ok, "usage should be present in metadata") + assert.Equal(float64(16), usage["output_tokens"], "output_tokens should equal max_output_tokens") + assert.Greater(usage["input_tokens"].(float64), float64(0)) + assert.Greater(usage["total_tokens"].(float64), float64(0)) + + incompleteDetails, ok := metadata["incomplete_details"].(map[string]interface{}) + require.True(ok, "incomplete_details should be present in metadata") + assert.Equal("max_output_tokens", incompleteDetails["reason"]) + + metrics := ts.Metrics() + assert.Greater(metrics["tokens"], float64(0)) + assert.GreaterOrEqual(metrics["time_to_first_token"], float64(0)) +} + func TestOpenAIResponsesWithListInput(t *testing.T) { client, _, exporter := setUpTest(t) assert := assert.New(t)