/* * Copyright 2025 CloudWeGo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package openai import ( "fmt" "sort" ) type ResponseMetaExtension struct { ID string `json:"id,omitempty"` Status ResponseStatus `json:"status,omitempty"` Error *ResponseError `json:"error,omitempty"` IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"` PreviousResponseID string `json:"previous_response_id,omitempty"` Reasoning *Reasoning `json:"reasoning,omitempty"` ServiceTier ServiceTier `json:"service_tier,omitempty"` CreatedAt int64 `json:"created_at,omitempty"` PromptCacheRetention PromptCacheRetention `json:"prompt_cache_retention,omitempty"` } type AssistantGenTextExtension struct { Refusal *OutputRefusal `json:"refusal,omitempty"` Annotations []*TextAnnotation `json:"annotations,omitempty"` } type ReasoningExtension struct { // Content is the reasoning text content. Content []*ReasoningContent `json:"content,omitempty"` } type ReasoningContent struct { // Index specifies the index position of this content in the final response. // Only available in streaming response. Index *int `json:"index,omitempty"` Text string `json:"text,omitempty"` } type ResponseError struct { Code ResponseErrorCode `json:"code,omitempty"` Message string `json:"message,omitempty"` } type IncompleteDetails struct { Reason string `json:"reason,omitempty"` } type Reasoning struct { Effort ReasoningEffort `json:"effort,omitempty"` Summary ReasoningSummary `json:"summary,omitempty"` } type OutputRefusal struct { Reason string `json:"reason,omitempty"` } type TextAnnotation struct { // Index specifies the index position of this annotation in the final response. // Only available in streaming response. Index int `json:"index,omitempty"` Type TextAnnotationType `json:"type,omitempty"` FileCitation *TextAnnotationFileCitation `json:"file_citation,omitempty"` URLCitation *TextAnnotationURLCitation `json:"url_citation,omitempty"` ContainerFileCitation *TextAnnotationContainerFileCitation `json:"container_file_citation,omitempty"` FilePath *TextAnnotationFilePath `json:"file_path,omitempty"` } type TextAnnotationFileCitation struct { // The ID of the file. FileID string `json:"file_id,omitempty"` // The filename of the file cited. Filename string `json:"filename,omitempty"` // The index of the file in the list of files. Index int `json:"index,omitempty"` } type TextAnnotationURLCitation struct { // The title of the web resource. Title string `json:"title,omitempty"` // The URL of the web resource. URL string `json:"url,omitempty"` // The index of the first character of the URL citation in the message. StartIndex int `json:"start_index,omitempty"` // The index of the last character of the URL citation in the message. EndIndex int `json:"end_index,omitempty"` } type TextAnnotationContainerFileCitation struct { // The ID of the container file. ContainerID string `json:"container_id,omitempty"` // The ID of the file. FileID string `json:"file_id,omitempty"` // The filename of the container file cited. Filename string `json:"filename,omitempty"` // The index of the first character of the container file citation in the message. StartIndex int `json:"start_index,omitempty"` // The index of the last character of the container file citation in the message. EndIndex int `json:"end_index,omitempty"` } type TextAnnotationFilePath struct { // The ID of the file. FileID string `json:"file_id,omitempty"` // The index of the file in the list of files. Index int `json:"index,omitempty"` } // ConcatAssistantGenTextExtensions concatenates multiple AssistantGenTextExtension chunks into a single one. func ConcatAssistantGenTextExtensions(chunks []*AssistantGenTextExtension) (*AssistantGenTextExtension, error) { if len(chunks) == 0 { return nil, fmt.Errorf("no assistant generated text extension found") } ret := &AssistantGenTextExtension{} var allAnnotations []*TextAnnotation for _, ext := range chunks { allAnnotations = append(allAnnotations, ext.Annotations...) } var ( indices []int indexToAnnotation = map[int]*TextAnnotation{} ) for _, an := range allAnnotations { if an == nil { continue } if indexToAnnotation[an.Index] == nil { indexToAnnotation[an.Index] = an indices = append(indices, an.Index) } else { return nil, fmt.Errorf("duplicate annotation index %d", an.Index) } } sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] }) ret.Annotations = make([]*TextAnnotation, 0, len(indices)) for _, idx := range indices { an := *indexToAnnotation[idx] an.Index = 0 // clear index ret.Annotations = append(ret.Annotations, &an) } for _, ext := range chunks { if ext.Refusal == nil { continue } if ret.Refusal == nil { ret.Refusal = ext.Refusal } else { ret.Refusal.Reason += ext.Refusal.Reason } } return ret, nil } // ConcatReasoningExtensions concatenates multiple ReasoningExtension chunks into a single one. func ConcatReasoningExtensions(chunks []*ReasoningExtension) (*ReasoningExtension, error) { if len(chunks) == 0 { return nil, fmt.Errorf("no reasoning extension found") } ret := &ReasoningExtension{} var ( indices []int indexToContent = map[int]*ReasoningContent{} hasIndexed bool hasUnindexed bool ) for _, ext := range chunks { if ext == nil { continue } for _, c := range ext.Content { if c == nil { continue } if c.Index == nil { hasUnindexed = true ret.Content = append(ret.Content, &ReasoningContent{Text: c.Text}) continue } hasIndexed = true idx := *c.Index if existing, ok := indexToContent[idx]; ok { existing.Text += c.Text } else { indexToContent[idx] = &ReasoningContent{Text: c.Text} indices = append(indices, idx) } } } if hasIndexed && hasUnindexed { return nil, fmt.Errorf("reasoning content chunks mix indexed and non-indexed content") } if hasIndexed { sort.Ints(indices) ret.Content = make([]*ReasoningContent, 0, len(indices)) for _, idx := range indices { ret.Content = append(ret.Content, indexToContent[idx]) } } return ret, nil } // ConcatResponseMetaExtensions concatenates multiple ResponseMetaExtension chunks into a single one. func ConcatResponseMetaExtensions(chunks []*ResponseMetaExtension) (*ResponseMetaExtension, error) { if len(chunks) == 0 { return nil, fmt.Errorf("no response meta extension found") } if len(chunks) == 1 { return chunks[0], nil } ret := &ResponseMetaExtension{} for _, ext := range chunks { if ext.ID != "" { ret.ID = ext.ID } if ext.Status != "" { ret.Status = ext.Status } if ext.Error != nil { ret.Error = ext.Error } if ext.IncompleteDetails != nil { ret.IncompleteDetails = ext.IncompleteDetails } if ext.PreviousResponseID != "" { ret.PreviousResponseID = ext.PreviousResponseID } if ext.Reasoning != nil { ret.Reasoning = ext.Reasoning } if ext.ServiceTier != "" { ret.ServiceTier = ext.ServiceTier } if ext.CreatedAt != 0 { ret.CreatedAt = ext.CreatedAt } if ext.PromptCacheRetention != "" { ret.PromptCacheRetention = ext.PromptCacheRetention } } return ret, nil }