package openai import ( "strings" "github.com/maximhq/bifrost/core/providers/utils" "github.com/maximhq/bifrost/core/schemas" ) // ToBifrostResponsesRequest converts an OpenAI responses request to Bifrost format func (resp *OpenAIResponsesRequest) ToBifrostResponsesRequest(ctx *schemas.BifrostContext) *schemas.BifrostResponsesRequest { if resp == nil { return nil } defaultProvider := schemas.OpenAI // for requests coming from azure sdk without provider prefix, we need to set the default provider to azure if ctx != nil { if isAzureUser, ok := ctx.Value(schemas.BifrostContextKeyIsAzureUserAgent).(bool); ok && isAzureUser { defaultProvider = schemas.Azure } } provider, model := schemas.ParseModelString(resp.Model, utils.CheckAndSetDefaultProvider(ctx, defaultProvider)) input := resp.Input.OpenAIResponsesRequestInputArray if len(input) == 0 { input = []schemas.ResponsesMessage{ { Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser), Content: &schemas.ResponsesMessageContent{ContentStr: resp.Input.OpenAIResponsesRequestInputStr}, }, } } return &schemas.BifrostResponsesRequest{ Provider: provider, Model: model, Input: input, Params: &resp.ResponsesParameters, Fallbacks: schemas.ParseFallbacks(resp.Fallbacks), } } // ToOpenAIResponsesRequest converts a Bifrost responses request to OpenAI format func ToOpenAIResponsesRequest(bifrostReq *schemas.BifrostResponsesRequest) *OpenAIResponsesRequest { if bifrostReq == nil || bifrostReq.Input == nil { return nil } var messages []schemas.ResponsesMessage // OpenAI models (except for gpt-oss) do not support reasoning content blocks, so we need to convert them to summaries, if there are any // OpenAI also doesn't support compaction content blocks, so we need to convert them to text blocks messages = make([]schemas.ResponsesMessage, 0, len(bifrostReq.Input)) for _, message := range bifrostReq.Input { // First, check if message has compaction content blocks and convert them to text if message.Content != nil && len(message.Content.ContentBlocks) > 0 { hasCompaction := false for _, block := range message.Content.ContentBlocks { if block.Type == schemas.ResponsesOutputMessageContentTypeCompaction { hasCompaction = true break } } if hasCompaction { // Create a new message with converted content blocks newMessage := message newContentBlocks := make([]schemas.ResponsesMessageContentBlock, 0, len(message.Content.ContentBlocks)) for _, block := range message.Content.ContentBlocks { if block.Type == schemas.ResponsesOutputMessageContentTypeCompaction { // Convert compaction block to text block if block.ResponsesOutputMessageContentCompaction != nil && block.ResponsesOutputMessageContentCompaction.Summary != "" { newContentBlocks = append(newContentBlocks, schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeText, Text: schemas.Ptr(block.ResponsesOutputMessageContentCompaction.Summary), }) } // If summary is empty, skip the block entirely } else { // Keep non-compaction blocks as-is newContentBlocks = append(newContentBlocks, block) } } // Only update if we have blocks remaining after conversion if len(newContentBlocks) > 0 { newMessage.Content = &schemas.ResponsesMessageContent{ ContentBlocks: newContentBlocks, } message = newMessage } else { // If all blocks were compaction with empty summaries, skip message continue } } } if message.ResponsesReasoning != nil { isGptOss := strings.Contains(bifrostReq.Model, "gpt-oss") isReasoning := isOpenAIReasoningModel(bifrostReq.Model) // For non-gpt-oss models, skip reasoning-only messages that have content blocks but no summaries. // For non-reasoning models (e.g., gpt-4o), also skip when EncryptedContent is present since // these models don't produce encrypted reasoning — any encrypted content is cross-provider // (e.g., Gemini ThoughtSignatures) and cannot be decrypted by OpenAI. if len(message.ResponsesReasoning.Summary) == 0 && message.Content != nil && len(message.Content.ContentBlocks) > 0 && !isGptOss && (message.ResponsesReasoning.EncryptedContent == nil || !isReasoning) { continue } // If the message has summaries but no content blocks and the model is gpt-oss, then convert the summaries to content blocks if len(message.ResponsesReasoning.Summary) > 0 && isGptOss && (message.Content == nil || len(message.Content.ContentBlocks) == 0) { var newMessage schemas.ResponsesMessage newMessage.ID = message.ID newMessage.Type = message.Type newMessage.Status = message.Status newMessage.Role = message.Role // Convert summaries to content blocks contentBlocks := make([]schemas.ResponsesMessageContentBlock, 0, len(message.ResponsesReasoning.Summary)) for _, summary := range message.ResponsesReasoning.Summary { contentBlocks = append(contentBlocks, schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeReasoning, Text: schemas.Ptr(summary.Text), }) } newMessage.Content = &schemas.ResponsesMessageContent{ ContentBlocks: contentBlocks, } messages = append(messages, newMessage) } else { // Clone the embedded pointer to avoid mutating the original input reasoningCopy := *message.ResponsesReasoning message.ResponsesReasoning = &reasoningCopy // OpenAI's Responses API does not accept 'role' on reasoning items message.Role = nil // Strip cross-provider encrypted content that non-reasoning models cannot decrypt. // Reasoning models (o1/o3/o4/GPT-5) may use EncryptedContent for multi-turn state. if !isReasoning { message.ResponsesReasoning.EncryptedContent = nil } messages = append(messages, message) } } else if message.ResponsesToolMessage != nil && message.ResponsesToolMessage.Action != nil && message.ResponsesToolMessage.Action.ResponsesComputerToolCallAction != nil { action := message.ResponsesToolMessage.Action.ResponsesComputerToolCallAction if action.Type == "zoom" || action.Region != nil { // Copy action and modify newAction := *action newAction.Region = nil if newAction.Type == "zoom" { newAction.Type = "screenshot" } actionStructCopy := *message.ResponsesToolMessage.Action actionStructCopy.ResponsesComputerToolCallAction = &newAction toolMsgCopy := *message.ResponsesToolMessage toolMsgCopy.Action = &actionStructCopy message.ResponsesToolMessage = &toolMsgCopy } messages = append(messages, message) } else { messages = append(messages, message) } } // Updating params params := bifrostReq.Params // Create the responses request with properly mapped parameters req := &OpenAIResponsesRequest{ Model: bifrostReq.Model, Input: OpenAIResponsesRequestInput{ OpenAIResponsesRequestInputArray: messages, }, } if params != nil { req.ResponsesParameters = *params if req.ResponsesParameters.MaxOutputTokens != nil && *req.ResponsesParameters.MaxOutputTokens < MinMaxCompletionTokens { req.ResponsesParameters.MaxOutputTokens = schemas.Ptr(MinMaxCompletionTokens) } // Drop user field if it exceeds OpenAI's 64 character limit req.ResponsesParameters.User = SanitizeUserField(req.ResponsesParameters.User) // Handle reasoning parameter: OpenAI uses effort-based reasoning // Priority: effort (native) > max_tokens (estimated) if req.ResponsesParameters.Reasoning != nil { // Clone the Reasoning pointer to avoid mutating the original params reasoningCopy := *req.ResponsesParameters.Reasoning req.ResponsesParameters.Reasoning = &reasoningCopy if req.ResponsesParameters.Reasoning.Effort != nil { // Native field is provided, use it (and clear max_tokens) effort := *req.ResponsesParameters.Reasoning.Effort // Convert "minimal" to "low"; cap "xhigh"/"max" to "high" — OpenAI tops out at high. switch effort { case "minimal": req.ResponsesParameters.Reasoning.Effort = schemas.Ptr("low") case "xhigh", "max": req.ResponsesParameters.Reasoning.Effort = schemas.Ptr("high") } // Clear max_tokens since OpenAI doesn't use it req.ResponsesParameters.Reasoning.MaxTokens = nil } else if req.ResponsesParameters.Reasoning.MaxTokens != nil { // Estimate effort from max_tokens maxTokens := *req.ResponsesParameters.Reasoning.MaxTokens maxOutputTokens := utils.GetMaxOutputTokensOrDefault(req.Model, DefaultCompletionMaxTokens) if req.ResponsesParameters.MaxOutputTokens != nil { maxOutputTokens = *req.ResponsesParameters.MaxOutputTokens } effort := utils.GetReasoningEffortFromBudgetTokens(maxTokens, MinReasoningMaxTokens, maxOutputTokens) req.ResponsesParameters.Reasoning.Effort = schemas.Ptr(effort) // Clear max_tokens since OpenAI doesn't use it req.ResponsesParameters.Reasoning.MaxTokens = nil } // summary:"none" is Anthropic-specific (maps to display:"omitted"); strip it for OpenAI. if req.ResponsesParameters.Reasoning.Summary != nil && *req.ResponsesParameters.Reasoning.Summary == "none" { req.ResponsesParameters.Reasoning.Summary = nil } // Handle xAI-specific parameter filtering // Only grok-3-mini supports reasoning_effort if bifrostReq.Provider == schemas.XAI && schemas.IsGrokReasoningModel(bifrostReq.Model) && !strings.Contains(bifrostReq.Model, "grok-3-mini") { // Clear reasoning_effort for non-grok-3-mini xAI reasoning models req.ResponsesParameters.Reasoning.Effort = nil } // Handle OpenAI-specific parameter filtering // Only o1/o3 series models support reasoning.effort // Regular models like gpt-4o, gpt-4, gpt-3.5-turbo don't support it if bifrostReq.Provider == schemas.OpenAI && !isOpenAIReasoningModel(bifrostReq.Model) { // Clear reasoning for non-reasoning OpenAI models to avoid API errors req.ResponsesParameters.Reasoning = nil } } // Strip top_p for OpenAI reasoning models (o1/o3 series) which reject it // GPT-5.x accept top_p when reasoning.effort is "none" (defaults to "none" when omitted) if isOpenAIReasoningModel(bifrostReq.Model) { stripTopP := true _, parsedModel := schemas.ParseModelString(bifrostReq.Model, schemas.OpenAI) modelLower := strings.ToLower(parsedModel) effort := "" if req.ResponsesParameters.Reasoning != nil && req.ResponsesParameters.Reasoning.Effort != nil { effort = *req.ResponsesParameters.Reasoning.Effort } // GPT-5.x: reasoning defaults to "none" when omitted, and top_p is allowed in that case // Exception: -pro and -codex variants always reason (no "none" mode), so top_p must be stripped if strings.HasPrefix(modelLower, "gpt-5.") && (effort == "" || effort == "none") && !strings.Contains(modelLower, "-pro") && !strings.Contains(modelLower, "-codex") { stripTopP = false } if stripTopP { req.ResponsesParameters.TopP = nil } } // Normalize function tool parameters for deterministic JSON serialization. // We must copy the Tools slice since it shares the backing array with bifrostReq.Params.Tools. if len(req.Tools) > 0 { normalizedTools := make([]schemas.ResponsesTool, len(req.Tools)) copy(normalizedTools, req.Tools) for i, tool := range normalizedTools { if tool.Type == schemas.ResponsesToolTypeFunction && tool.ResponsesToolFunction != nil && tool.ResponsesToolFunction.Parameters != nil { funcCopy := *tool.ResponsesToolFunction funcCopy.Parameters = tool.ResponsesToolFunction.Parameters.Normalized() normalizedTools[i].ResponsesToolFunction = &funcCopy } } req.Tools = normalizedTools } // Filter out tools that OpenAI doesn't support req.filterUnsupportedTools() } if bifrostReq.Params != nil { req.ExtraParams = bifrostReq.Params.ExtraParams } return req } // filterUnsupportedTools removes tool types that OpenAI doesn't support func (resp *OpenAIResponsesRequest) filterUnsupportedTools() { if len(resp.Tools) == 0 { return } // Define OpenAI-supported tool types supportedTypes := map[schemas.ResponsesToolType]bool{ schemas.ResponsesToolTypeFunction: true, schemas.ResponsesToolTypeFileSearch: true, schemas.ResponsesToolTypeComputerUsePreview: true, schemas.ResponsesToolTypeWebSearch: true, schemas.ResponsesToolTypeWebFetch: true, schemas.ResponsesToolTypeMCP: true, schemas.ResponsesToolTypeCodeInterpreter: true, schemas.ResponsesToolTypeImageGeneration: true, schemas.ResponsesToolTypeLocalShell: true, schemas.ResponsesToolTypeCustom: true, schemas.ResponsesToolTypeWebSearchPreview: true, schemas.ResponsesToolTypeMemory: true, schemas.ResponsesToolTypeToolSearch: true, } // Filter tools to only include supported types filteredTools := make([]schemas.ResponsesTool, 0, len(resp.Tools)) for _, tool := range resp.Tools { if supportedTypes[tool.Type] { // check for computer use preview if tool.Type == schemas.ResponsesToolTypeComputerUsePreview && tool.ResponsesToolComputerUsePreview != nil && tool.ResponsesToolComputerUsePreview.EnableZoom != nil { newTool := tool newComputerUse := &schemas.ResponsesToolComputerUsePreview{ DisplayHeight: tool.ResponsesToolComputerUsePreview.DisplayHeight, DisplayWidth: tool.ResponsesToolComputerUsePreview.DisplayWidth, Environment: tool.ResponsesToolComputerUsePreview.Environment, // EnableZoom is intentionally omitted (nil) - OpenAI doesn't support it } newTool.ResponsesToolComputerUsePreview = newComputerUse filteredTools = append(filteredTools, newTool) } else if tool.Type == schemas.ResponsesToolTypeWebSearch && tool.ResponsesToolWebSearch != nil { // Create a proper deep copy with new nested pointers to avoid mutating the original newTool := tool newWebSearch := &schemas.ResponsesToolWebSearch{} // MaxUses is intentionally omitted (nil) - OpenAI doesn't support it // Handle Filters: OpenAI doesn't support BlockedDomains or TimeRangeFilter if tool.ResponsesToolWebSearch.Filters != nil { hasAllowedDomains := len(tool.ResponsesToolWebSearch.Filters.AllowedDomains) > 0 if hasAllowedDomains { // Keep only AllowedDomains (copy the slice to avoid sharing) newWebSearch.Filters = &schemas.ResponsesToolWebSearchFilters{ AllowedDomains: append([]string(nil), tool.ResponsesToolWebSearch.Filters.AllowedDomains...), // BlockedDomains and TimeRangeFilter are intentionally omitted - OpenAI doesn't support it } } // If only blocked domains or both empty, Filters stays nil } // Copy other fields if they exist if tool.ResponsesToolWebSearch.UserLocation != nil { newWebSearch.UserLocation = tool.ResponsesToolWebSearch.UserLocation } if tool.ResponsesToolWebSearch.SearchContextSize != nil { newWebSearch.SearchContextSize = tool.ResponsesToolWebSearch.SearchContextSize } newTool.ResponsesToolWebSearch = newWebSearch filteredTools = append(filteredTools, newTool) } else { filteredTools = append(filteredTools, tool) } } } resp.Tools = filteredTools }