package dto import ( "encoding/json" "fmt" "strings" kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/QuantumNous/new-api/relaykit/types" ) const ( ResponsesOutputTypeImageGenerationCall = "image_generation_call" ) type SimpleResponse struct { Usage `json:"usage"` Error any `json:"error"` } // GetOpenAIError 从动态错误类型中提取OpenAIError结构 func (s *SimpleResponse) GetOpenAIError() *types.OpenAIError { return GetOpenAIError(s.Error) } type TextResponse struct { Id string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []OpenAITextResponseChoice `json:"choices"` Usage `json:"usage"` } type OpenAITextResponseChoice struct { Index int `json:"index"` Message `json:"message"` FinishReason string `json:"finish_reason"` } type OpenAITextResponse struct { Id string `json:"id"` Model string `json:"model"` Object string `json:"object"` Created any `json:"created"` Choices []OpenAITextResponseChoice `json:"choices"` Error any `json:"error,omitempty"` Usage `json:"usage"` } // GetOpenAIError 从动态错误类型中提取OpenAIError结构 func (o *OpenAITextResponse) GetOpenAIError() *types.OpenAIError { return GetOpenAIError(o.Error) } type OpenAIEmbeddingResponseItem struct { Object string `json:"object"` Index int `json:"index"` Embedding []float64 `json:"embedding"` } type OpenAIEmbeddingResponse struct { Object string `json:"object"` Data []OpenAIEmbeddingResponseItem `json:"data"` Model string `json:"model"` Usage `json:"usage"` } type FlexibleEmbeddingResponseItem struct { Object string `json:"object"` Index int `json:"index"` Embedding any `json:"embedding"` } type FlexibleEmbeddingResponse struct { Object string `json:"object"` Data []FlexibleEmbeddingResponseItem `json:"data"` Model string `json:"model"` Usage `json:"usage"` } type ChatCompletionsStreamResponseChoice struct { Delta ChatCompletionsStreamResponseChoiceDelta `json:"delta,omitempty"` Logprobs *any `json:"logprobs"` FinishReason *string `json:"finish_reason"` Index int `json:"index"` } type ChatCompletionsStreamResponseChoiceDelta struct { Content *string `json:"content,omitempty"` ReasoningContent *string `json:"reasoning_content,omitempty"` Reasoning *string `json:"reasoning,omitempty"` Role string `json:"role,omitempty"` ToolCalls []ToolCallResponse `json:"tool_calls,omitempty"` // Annotations is an OpenAI-compatible streaming extension supported by // providers such as OpenRouter. Relaykit uses it to preserve streaming URL // citations, including Claude round-trip metadata. Annotations json.RawMessage `json:"annotations,omitempty"` } func (c *ChatCompletionsStreamResponseChoiceDelta) SetContentString(s string) { c.Content = &s } func (c *ChatCompletionsStreamResponseChoiceDelta) GetContentString() string { if c.Content == nil { return "" } return *c.Content } func (c *ChatCompletionsStreamResponseChoiceDelta) GetReasoningContent() string { if c.ReasoningContent == nil && c.Reasoning == nil { return "" } if c.ReasoningContent != nil { return *c.ReasoningContent } return *c.Reasoning } func (c *ChatCompletionsStreamResponseChoiceDelta) SetReasoningContent(s string) { c.ReasoningContent = &s //c.Reasoning = &s } type ToolCallResponse struct { // Index is not nil only in chat completion chunk object Index *int `json:"index,omitempty"` ID string `json:"id,omitempty"` Type any `json:"type"` Function FunctionResponse `json:"function"` } func (c *ToolCallResponse) SetIndex(i int) { c.Index = &i } type FunctionResponse struct { Description string `json:"description,omitempty"` Name string `json:"name,omitempty"` // call function with arguments in JSON format Parameters any `json:"parameters,omitempty"` // request Arguments string `json:"arguments"` // response } type ChatCompletionsStreamResponse struct { Id string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` SystemFingerprint *string `json:"system_fingerprint"` Choices []ChatCompletionsStreamResponseChoice `json:"choices"` Usage *Usage `json:"usage"` } func (c *ChatCompletionsStreamResponse) IsFinished() bool { if len(c.Choices) == 0 { return false } return c.Choices[0].FinishReason != nil && *c.Choices[0].FinishReason != "" } func (c *ChatCompletionsStreamResponse) IsToolCall() bool { if len(c.Choices) == 0 { return false } return len(c.Choices[0].Delta.ToolCalls) > 0 } func (c *ChatCompletionsStreamResponse) GetFirstToolCall() *ToolCallResponse { if c.IsToolCall() { return &c.Choices[0].Delta.ToolCalls[0] } return nil } func (c *ChatCompletionsStreamResponse) ClearToolCalls() { if !c.IsToolCall() { return } for choiceIdx := range c.Choices { for callIdx := range c.Choices[choiceIdx].Delta.ToolCalls { c.Choices[choiceIdx].Delta.ToolCalls[callIdx].ID = "" c.Choices[choiceIdx].Delta.ToolCalls[callIdx].Type = nil c.Choices[choiceIdx].Delta.ToolCalls[callIdx].Function.Name = "" } } } func (c *ChatCompletionsStreamResponse) Copy() *ChatCompletionsStreamResponse { choices := make([]ChatCompletionsStreamResponseChoice, len(c.Choices)) copy(choices, c.Choices) return &ChatCompletionsStreamResponse{ Id: c.Id, Object: c.Object, Created: c.Created, Model: c.Model, SystemFingerprint: c.SystemFingerprint, Choices: choices, Usage: c.Usage, } } func (c *ChatCompletionsStreamResponse) GetSystemFingerprint() string { if c.SystemFingerprint == nil { return "" } return *c.SystemFingerprint } func (c *ChatCompletionsStreamResponse) SetSystemFingerprint(s string) { c.SystemFingerprint = &s } type ChatCompletionsStreamResponseSimple struct { Choices []ChatCompletionsStreamResponseChoice `json:"choices"` Usage *Usage `json:"usage"` } type CompletionsStreamResponse struct { Choices []struct { Text string `json:"text"` FinishReason string `json:"finish_reason"` } `json:"choices"` } type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` PromptCacheHitTokens int `json:"prompt_cache_hit_tokens,omitempty"` UsageSemantic string `json:"usage_semantic,omitempty"` UsageSource string `json:"usage_source,omitempty"` BillingUsage *BillingUsage `json:"billing_usage,omitempty"` PromptTokensDetails InputTokenDetails `json:"prompt_tokens_details"` CompletionTokenDetails OutputTokenDetails `json:"completion_tokens_details"` InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` InputTokensDetails *InputTokenDetails `json:"input_tokens_details"` // claude cache 1h ClaudeCacheCreation5mTokens int `json:"claude_cache_creation_5_m_tokens"` ClaudeCacheCreation1hTokens int `json:"claude_cache_creation_1_h_tokens"` // OpenRouter Params Cost any `json:"cost,omitempty"` } type OpenAIVideoResponse struct { Id string `json:"id" example:"file-abc123"` Object string `json:"object" example:"file"` Bytes int64 `json:"bytes" example:"120000"` CreatedAt int64 `json:"created_at" example:"1677610602"` ExpiresAt int64 `json:"expires_at" example:"1677614202"` Filename string `json:"filename" example:"mydata.jsonl"` Purpose string `json:"purpose" example:"fine-tune"` } type InputTokenDetails struct { CachedTokens int `json:"cached_tokens"` CachedCreationTokens int `json:"cached_creation_tokens,omitempty"` // CacheWriteTokens is OpenAI's native cache-write count, reported as // prompt_tokens_details.cache_write_tokens (Chat Completions) or // input_tokens_details.cache_write_tokens (Responses). It is billed at the // cache-creation price. CacheWriteTokens int `json:"cache_write_tokens,omitempty"` TextTokens int `json:"text_tokens"` AudioTokens int `json:"audio_tokens"` ImageTokens int `json:"image_tokens"` } // CacheCreationTokensTotal returns the cache-write token count regardless of // which field the upstream reported it in: Claude-derived conversions populate // CachedCreationTokens while OpenAI reports cache_write_tokens natively. Both // are billed at the cache-creation price; when both are present the larger // value wins so the same tokens are never double-counted. Negative upstream // values are clamped to zero so they can never lower a charge. func (d InputTokenDetails) CacheCreationTokensTotal() int { total := d.CachedCreationTokens if d.CacheWriteTokens > total { total = d.CacheWriteTokens } if total < 0 { return 0 } return total } type OutputTokenDetails struct { TextTokens int `json:"text_tokens"` AudioTokens int `json:"audio_tokens"` ImageTokens int `json:"image_tokens"` ReasoningTokens int `json:"reasoning_tokens"` } type OpenAIResponsesResponse struct { ID string `json:"id"` Object string `json:"object"` CreatedAt int `json:"created_at"` Status json.RawMessage `json:"status"` Error any `json:"error,omitempty"` IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"` Instructions json.RawMessage `json:"instructions"` MaxOutputTokens int `json:"max_output_tokens"` Model string `json:"model"` Output []ResponsesOutput `json:"output"` ParallelToolCalls bool `json:"parallel_tool_calls"` PreviousResponseID json.RawMessage `json:"previous_response_id"` Reasoning *Reasoning `json:"reasoning"` Store bool `json:"store"` Temperature float64 `json:"temperature"` ToolChoice json.RawMessage `json:"tool_choice"` Tools []map[string]any `json:"tools"` TopP float64 `json:"top_p"` Truncation json.RawMessage `json:"truncation"` Usage *Usage `json:"usage"` User json.RawMessage `json:"user"` Metadata json.RawMessage `json:"metadata"` } // GetOpenAIError 从动态错误类型中提取OpenAIError结构 func (o *OpenAIResponsesResponse) GetOpenAIError() *types.OpenAIError { return GetOpenAIError(o.Error) } type IncompleteDetails struct { Reason string `json:"reason"` } type ResponsesOutput struct { Type string `json:"type"` ID string `json:"id"` Status string `json:"status"` Role string `json:"role"` Content []ResponsesOutputContent `json:"content"` Summary []ResponsesReasoningSummaryPart `json:"summary,omitempty"` Quality string `json:"quality"` Size string `json:"size"` Result string `json:"result,omitempty"` CallId string `json:"call_id,omitempty"` Name string `json:"name,omitempty"` Arguments json.RawMessage `json:"arguments,omitempty"` Action json.RawMessage `json:"action,omitempty"` Queries json.RawMessage `json:"queries,omitempty"` Results json.RawMessage `json:"results,omitempty"` Sources json.RawMessage `json:"sources,omitempty"` Code json.RawMessage `json:"code,omitempty"` Outputs json.RawMessage `json:"outputs,omitempty"` ContainerID string `json:"container_id,omitempty"` PendingSafetyChecks json.RawMessage `json:"pending_safety_checks,omitempty"` Caller json.RawMessage `json:"caller,omitempty"` ServerLabel string `json:"server_label,omitempty"` Output json.RawMessage `json:"output,omitempty"` ItemError json.RawMessage `json:"error,omitempty"` ApprovalRequestID string `json:"approval_request_id,omitempty"` MCPTools json.RawMessage `json:"tools,omitempty"` } // MarshalJSON keeps hosted-tool variants within their protocol-specific // schemas. ResponsesOutput also represents messages, images, and function // calls, whose fields must not leak into web_search_call or mcp_call items. func (r ResponsesOutput) MarshalJSON() ([]byte, error) { switch r.Type { case "web_search_call": return kitutil.Marshal(struct { Type string `json:"type"` ID string `json:"id"` Status string `json:"status,omitempty"` Action json.RawMessage `json:"action,omitempty"` }{Type: r.Type, ID: r.ID, Status: r.Status, Action: r.Action}) case "mcp_call": return kitutil.Marshal(struct { Type string `json:"type"` ID string `json:"id"` Name string `json:"name"` ServerLabel string `json:"server_label"` Arguments json.RawMessage `json:"arguments"` Status string `json:"status,omitempty"` Output json.RawMessage `json:"output,omitempty"` Error json.RawMessage `json:"error,omitempty"` ApprovalRequestID string `json:"approval_request_id,omitempty"` }{ Type: r.Type, ID: r.ID, Name: r.Name, ServerLabel: r.ServerLabel, Arguments: r.Arguments, Status: r.Status, Output: r.Output, Error: r.ItemError, ApprovalRequestID: r.ApprovalRequestID, }) default: type responsesOutputAlias ResponsesOutput return kitutil.Marshal(responsesOutputAlias(r)) } } // NormalizeResponsesWebSearchAction validates and canonicalizes the current // Responses web_search_call action union. Claude emits {"query": ...}; the // Responses representation additionally requires a discriminator. func NormalizeResponsesWebSearchAction(raw json.RawMessage) (json.RawMessage, error) { var action struct { Type string `json:"type"` Query string `json:"query"` Queries []string `json:"queries"` Sources json.RawMessage `json:"sources"` URL string `json:"url"` Pattern string `json:"pattern"` } if err := kitutil.Unmarshal(raw, &action); err != nil { return nil, fmt.Errorf("decode Responses web-search action: %w", err) } action.Type = strings.TrimSpace(action.Type) action.Query = strings.TrimSpace(action.Query) action.URL = strings.TrimSpace(action.URL) action.Pattern = strings.TrimSpace(action.Pattern) for index := range action.Queries { action.Queries[index] = strings.TrimSpace(action.Queries[index]) if action.Queries[index] == "" { return nil, fmt.Errorf("Responses web-search action queries[%d] must not be empty", index) } } if action.Type == "" && (action.Query != "" || len(action.Queries) > 0) { action.Type = "search" } var canonical any switch action.Type { case "search": if action.Query == "" && len(action.Queries) == 0 { return nil, fmt.Errorf("Responses web-search action %q requires query or queries", action.Type) } if len(action.Sources) > 0 && kitutil.GetJsonType(action.Sources) != "array" && kitutil.GetJsonType(action.Sources) != "null" { return nil, fmt.Errorf("Responses web-search action sources must be an array") } canonical = struct { Type string `json:"type"` Query string `json:"query,omitempty"` Queries []string `json:"queries,omitempty"` Sources json.RawMessage `json:"sources,omitempty"` }{Type: action.Type, Query: action.Query, Queries: action.Queries, Sources: action.Sources} case "open_page": if action.URL == "" { return nil, fmt.Errorf("Responses web-search action %q requires url", action.Type) } canonical = struct { Type string `json:"type"` URL string `json:"url"` }{Type: action.Type, URL: action.URL} case "find", "find_in_page": if action.URL == "" || action.Pattern == "" { return nil, fmt.Errorf("Responses web-search action %q requires url and pattern", action.Type) } canonical = struct { Type string `json:"type"` URL string `json:"url"` Pattern string `json:"pattern"` }{Type: "find_in_page", URL: action.URL, Pattern: action.Pattern} default: return nil, fmt.Errorf("unsupported Responses web-search action type %q", action.Type) } encoded, err := kitutil.Marshal(canonical) if err != nil { return nil, fmt.Errorf("encode Responses web-search action: %w", err) } return encoded, nil } // ArgumentsString returns function call arguments in the string form expected by Chat Completions. func (r *ResponsesOutput) ArgumentsString() string { if r == nil { return "" } return ResponsesArgumentsString(r.Arguments) } // ResponsesArgumentsString returns function call arguments in the string form expected by Chat Completions. func ResponsesArgumentsString(arguments json.RawMessage) string { return kitutil.JsonRawMessageToString(arguments) } type ResponsesOutputContent struct { Type string `json:"type"` Text string `json:"text"` Annotations []interface{} `json:"annotations"` } type ResponsesReasoningSummaryPart struct { Type string `json:"type"` Text string `json:"text"` } const ( BuildInToolWebSearchPreview = "web_search_preview" BuildInToolWebSearch = "web_search" BuildInToolFileSearch = "file_search" BuildInToolGoogleSearch = "google_search" BuildInToolImageGeneration = "image_generation" ) const ( BuildInCallWebSearchCall = "web_search_call" BuildInCallFileSearchCall = "file_search_call" BuildInCallFunctionCall = "function_call" BuildInCallToolUse = "tool_use" ) const ( ResponsesOutputTypeItemAdded = "response.output_item.added" ResponsesOutputTypeItemDone = "response.output_item.done" ) // ResponsesStreamResponse 用于处理 /v1/responses 流式响应 type ResponsesStreamResponse struct { Type string `json:"type"` Response *OpenAIResponsesResponse `json:"response,omitempty"` Code string `json:"code,omitempty"` Message string `json:"message,omitempty"` Param string `json:"param,omitempty"` Delta string `json:"delta,omitempty"` Arguments *string `json:"arguments,omitempty"` Name string `json:"name,omitempty"` Text *string `json:"text,omitempty"` Item *ResponsesOutput `json:"item,omitempty"` SequenceNumber *int `json:"sequence_number,omitempty"` Annotation json.RawMessage `json:"annotation,omitempty"` AnnotationIndex *int `json:"annotation_index,omitempty"` Obfuscation string `json:"obfuscation,omitempty"` // - response.function_call_arguments.delta // - response.function_call_arguments.done OutputIndex *int `json:"output_index,omitempty"` ContentIndex *int `json:"content_index,omitempty"` SummaryIndex *int `json:"summary_index,omitempty"` ItemID string `json:"item_id,omitempty"` Part *ResponsesReasoningSummaryPart `json:"part,omitempty"` } // GetOpenAIError 从动态错误类型中提取OpenAIError结构 func GetOpenAIError(errorField any) *types.OpenAIError { if errorField == nil { return nil } switch err := errorField.(type) { case types.OpenAIError: return &err case *types.OpenAIError: return err case map[string]interface{}: // 处理从JSON解析来的map结构 openaiErr := &types.OpenAIError{} if errType, ok := err["type"].(string); ok { openaiErr.Type = errType } if errMsg, ok := err["message"].(string); ok { openaiErr.Message = errMsg } if errParam, ok := err["param"].(string); ok { openaiErr.Param = errParam } if errCode, ok := err["code"]; ok { openaiErr.Code = errCode } return openaiErr case string: // 处理简单字符串错误 return &types.OpenAIError{ Type: "error", Message: err, } default: // 未知类型,尝试转换为字符串 return &types.OpenAIError{ Type: "unknown_error", Message: fmt.Sprintf("%v", err), } } }