Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion providers/openai/openai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4061,8 +4061,12 @@ func TestResponsesStream_WebSearchResponse(t *testing.T) {
`data: {"type":"response.output_item.added","output_index":1,"item":{"type":"message","id":"msg_01","role":"assistant","status":"in_progress","content":[]}}` + "\n\n",
"event: response.output_text.delta\n" +
`data: {"type":"response.output_text.delta","output_index":1,"content_index":0,"delta":"Here are the results."}` + "\n\n",
"event: response.output_text.annotation.added\n" +
`data: {"type":"response.output_text.annotation.added","annotation":{"type":"url_citation","url":"https://example.com/ai-news","title":"Latest AI News","start_index":0,"end_index":21},"annotation_index":0,"content_index":0,"item_id":"msg_01","output_index":1,"sequence_number":10}` + "\n\n",
"event: response.output_text.annotation.added\n" +
`data: {"type":"response.output_text.annotation.added","annotation":{"type":"url_citation","url":"https://example.com/more-news","title":"More AI News","start_index":22,"end_index":40},"annotation_index":1,"content_index":0,"item_id":"msg_01","output_index":1,"sequence_number":11}` + "\n\n",
"event: response.output_item.done\n" +
`data: {"type":"response.output_item.done","output_index":1,"item":{"type":"message","id":"msg_01","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Here are the results.","annotations":[{"type":"url_citation","url":"https://example.com/ai-news","title":"Latest AI News","start_index":0,"end_index":21}]}]}}` + "\n\n",
`data: {"type":"response.output_item.done","output_index":1,"item":{"type":"message","id":"msg_01","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Here are the results.","annotations":[{"type":"url_citation","url":"https://example.com/ai-news","title":"Latest AI News","start_index":0,"end_index":21},{"type":"url_citation","url":"https://example.com/more-news","title":"More AI News","start_index":22,"end_index":40}]}]}}` + "\n\n",
"event: response.completed\n" +
`data: {"type":"response.completed","response":{"id":"resp_01","status":"completed","output":[],"usage":{"input_tokens":100,"output_tokens":50,"total_tokens":150}}}` + "\n\n",
}
Expand Down Expand Up @@ -4090,6 +4094,7 @@ func TestResponsesStream_WebSearchResponse(t *testing.T) {
toolCalls []fantasy.StreamPart
toolResults []fantasy.StreamPart
textDeltas []fantasy.StreamPart
sources []fantasy.StreamPart
finishes []fantasy.StreamPart
)
for _, p := range parts {
Expand All @@ -4102,6 +4107,8 @@ func TestResponsesStream_WebSearchResponse(t *testing.T) {
toolResults = append(toolResults, p)
case fantasy.StreamPartTypeTextDelta:
textDeltas = append(textDeltas, p)
case fantasy.StreamPartTypeSource:
sources = append(sources, p)
case fantasy.StreamPartTypeFinish:
finishes = append(finishes, p)
}
Expand All @@ -4123,6 +4130,16 @@ func TestResponsesStream_WebSearchResponse(t *testing.T) {
require.NotEmpty(t, textDeltas, "should have text deltas")
require.Equal(t, "Here are the results.", textDeltas[0].Delta)

require.Len(t, sources, 2, "should have two source citations from annotation events")
require.Equal(t, fantasy.SourceTypeURL, sources[0].SourceType)
require.Equal(t, "https://example.com/ai-news", sources[0].URL)
require.Equal(t, "Latest AI News", sources[0].Title)
require.NotEmpty(t, sources[0].ID, "source should have an ID")
require.Equal(t, fantasy.SourceTypeURL, sources[1].SourceType)
require.Equal(t, "https://example.com/more-news", sources[1].URL)
require.Equal(t, "More AI News", sources[1].Title)
require.NotEmpty(t, sources[1].ID, "source should have an ID")

require.Len(t, finishes, 1)
responsesMeta, ok := finishes[0].ProviderMetadata[Name].(*ResponsesProviderMetadata)
require.True(t, ok)
Expand Down
37 changes: 37 additions & 0 deletions providers/openai/responses_language_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,43 @@ func (o responsesLanguageModel) Stream(ctx context.Context, call fantasy.Call) (
return
}

case "response.output_text.annotation.added":
added := event.AsResponseOutputTextAnnotationAdded()
// The Annotation field is typed as `any` in the SDK;
// it deserializes as map[string]interface{} from JSON.
annotationMap, ok := added.Annotation.(map[string]interface{})
if !ok {
break
}
annotationType, _ := annotationMap["type"].(string)
switch annotationType {
case "url_citation":
url, _ := annotationMap["url"].(string)
title, _ := annotationMap["title"].(string)
if !yield(fantasy.StreamPart{
Type: fantasy.StreamPartTypeSource,
ID: uuid.NewString(),
SourceType: fantasy.SourceTypeURL,
URL: url,
Title: title,
}) {
return
}
case "file_citation":
title := "Document"
if fn, ok := annotationMap["filename"].(string); ok && fn != "" {
title = fn
}
if !yield(fantasy.StreamPart{
Type: fantasy.StreamPartTypeSource,
ID: uuid.NewString(),
SourceType: fantasy.SourceTypeDocument,
Title: title,
}) {
return
}
}

case "response.reasoning_summary_part.added":
added := event.AsResponseReasoningSummaryPartAdded()
state := activeReasoning[added.ItemID]
Expand Down