284 lines
7.9 KiB
Go
284 lines
7.9 KiB
Go
/*
|
|
* 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
|
|
}
|