diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index cb8859ed..195eff22 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -1120,6 +1120,9 @@ func responsesProcessResponse(resp *responses.Response, seqNum int64, yield func case responses.ResponseOutputText: textContent := &message.TextContent{Text: c.Text} populateAnnotations(c.Annotations, textContent) + if len(c.Logprobs) > 0 { + textContent.AdditionalProperties = map[string]any{"Logprobs": c.Logprobs} + } currentUpdate.Contents = append(currentUpdate.Contents, textContent) case responses.ResponseOutputRefusal: currentUpdate.Contents = append(currentUpdate.Contents, &message.ErrorContent{ @@ -1582,24 +1585,27 @@ func responsesProcessStreamingUpdate(update responses.ResponseStreamEventUnion, u.Role = state.role // For messages, only emit content if there are annotations that weren't in delta events // Delta events handle the text itself, but annotations only appear in done events - hasAnnotations := false + hasMetadata := false for _, c := range item.Content { - if c, ok := c.AsAny().(responses.ResponseOutputText); ok && len(c.Annotations) > 0 { - hasAnnotations = true + if c, ok := c.AsAny().(responses.ResponseOutputText); ok && (len(c.Annotations) > 0 || len(c.Logprobs) > 0) { + hasMetadata = true break } } - if hasAnnotations { + if hasMetadata { annotatedContent := &message.TextContent{} for _, c := range item.Content { if outputText, ok := c.AsAny().(responses.ResponseOutputText); ok { populateAnnotations(outputText.Annotations, annotatedContent) + if len(outputText.Logprobs) > 0 { + annotatedContent.AdditionalProperties = map[string]any{"Logprobs": outputText.Logprobs} + } } } u.Contents = []message.Content{annotatedContent} } - // If no annotations, don't emit content (delta events already did) + // If no annotations or logprobs, don't emit content (delta events already did) case responses.ResponseFunctionToolCall: state.anyFunctions = true diff --git a/provider/openaiprovider/responses_test.go b/provider/openaiprovider/responses_test.go index 5b1d7a2d..e7cc3821 100644 --- a/provider/openaiprovider/responses_test.go +++ b/provider/openaiprovider/responses_test.go @@ -153,6 +153,43 @@ func TestResponsesRequestIncludesAgentFrameworkUserAgent(t *testing.T) { } } +// output_text logprobs must be surfaced on the TextContent's +// AdditionalProperties, matching the Python client which stores them there. +func TestResponsesOutputTextLogprobsSurfaced_NonStreaming(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{ + "id":"resp_lp", + "object":"response", + "created_at":1741891428, + "status":"completed", + "error":null, + "incomplete_details":null, + "model":"gpt-4o-mini", + "output":[{"type":"message","id":"msg_lp","status":"completed","role":"assistant","content":[{"type":"output_text","text":"hello","annotations":[],"logprobs":[{"token":"hello","logprob":-0.05,"bytes":[104,101,108,108,111],"top_logprobs":[]}]}]}] + }`) + })) + defer server.Close() + + resp, err := newTestResponsesClient(server, "gpt-4o-mini").RunText(t.Context(), "hi").Collect() + if err != nil { + t.Fatalf("error = %v", err) + } + + var text *message.TextContent + for content := range resp.Contents() { + if tc, ok := content.(*message.TextContent); ok { + text = tc + } + } + if text == nil { + t.Fatalf("no TextContent in response") + } + if _, ok := text.AdditionalProperties["Logprobs"]; !ok { + t.Errorf("Logprobs missing from TextContent AdditionalProperties: %#v", text.AdditionalProperties) + } +} + func TestNewAgentCurrentlyUsesResponsesAPI(t *testing.T) { const input = ` {