package integrations import ( "bytes" "context" "encoding/json" "errors" "strconv" "strings" "time" "github.com/bytedance/sonic" bifrost "github.com/maximhq/bifrost/core" "github.com/maximhq/bifrost/core/providers/openai" "github.com/maximhq/bifrost/core/schemas" "github.com/maximhq/bifrost/transports/bifrost-http/lib" "github.com/valyala/fasthttp" ) // Chat completion chunk types for Cursor responses // These lightweight structs produce clean chat completion JSON without the // extra_fields that BifrostChatResponse would include. type cursorChatChunk struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []cursorChatChoice `json:"choices"` Usage *cursorUsage `json:"usage,omitempty"` } type cursorChatChoice struct { Index int `json:"index"` Delta cursorChatDelta `json:"delta"` FinishReason *string `json:"finish_reason"` } type cursorChatDelta struct { Role *string `json:"role,omitempty"` Content *string `json:"content,omitempty"` Reasoning *string `json:"reasoning,omitempty"` ToolCalls []cursorToolCallDelta `json:"tool_calls,omitempty"` } type cursorToolCallDelta struct { Index int `json:"index"` ID *string `json:"id,omitempty"` Type *string `json:"type,omitempty"` Function *cursorToolCallFnDelta `json:"function,omitempty"` } type cursorToolCallFnDelta struct { Name *string `json:"name,omitempty"` Arguments *string `json:"arguments,omitempty"` } type cursorUsage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } // Non-streaming chat completion types type cursorChatCompletion struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []cursorChatCompletionChoice `json:"choices"` Usage *cursorUsage `json:"usage,omitempty"` } type cursorChatCompletionChoice struct { Index int `json:"index"` Message cursorChatCompletionMessage `json:"message"` FinishReason string `json:"finish_reason"` } type cursorChatCompletionMessage struct { Role string `json:"role"` Content string `json:"content"` ToolCalls []cursorToolCall `json:"tool_calls,omitempty"` } type cursorToolCall struct { ID string `json:"id"` Type string `json:"type"` Function cursorToolCallFn `json:"function"` } type cursorToolCallFn struct { Name string `json:"name"` Arguments string `json:"arguments"` } // Converter helpers // cursorChunkID builds a deterministic chunk ID from the response extra fields. func cursorChunkID(extras *schemas.BifrostResponseExtraFields) string { return "chatcmpl-bifrost-" + strconv.Itoa(extras.ChunkIndex) } // cursorModel returns the best model name available from extra fields. func cursorModel(extras *schemas.BifrostResponseExtraFields) string { if extras.ResolvedModelUsed != "" { return extras.ResolvedModelUsed } return extras.OriginalModelRequested } // convertResponsesStreamToChatChunk maps a Responses API stream event to a // chat completion chunk. Returns ("", nil, nil) for events that should be skipped. func convertResponsesStreamToChatChunk(resp *schemas.BifrostResponsesStreamResponse) (string, interface{}, error) { switch resp.Type { case schemas.ResponsesStreamResponseTypeOutputItemAdded: if resp.Item == nil { return "", nil, nil } // Function call item → send first tool call chunk with id, type, and name // NOTE: This must be checked before the role branch because function_call // items can also carry role:"assistant", which would cause an early return // and prevent the tool-call id/type/name chunk from being emitted. if resp.Item.Type != nil && *resp.Item.Type == schemas.ResponsesMessageTypeFunctionCall && resp.Item.ResponsesToolMessage != nil { fnType := "function" toolCallIndex := 0 if resp.OutputIndex != nil { toolCallIndex = *resp.OutputIndex } tc := cursorToolCallDelta{ Index: toolCallIndex, ID: resp.Item.CallID, Type: &fnType, Function: &cursorToolCallFnDelta{Name: resp.Item.Name}, } return "", &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{ToolCalls: []cursorToolCallDelta{tc}}, }}, }, nil } // Assistant text output item → send role delta if resp.Item.Role != nil && *resp.Item.Role == schemas.ResponsesInputMessageRoleAssistant { role := "assistant" return "", &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{Role: &role}, }}, }, nil } return "", nil, nil case schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDelta: if resp.Delta == nil { return "", nil, nil } toolCallIndex := 0 if resp.OutputIndex != nil { toolCallIndex = *resp.OutputIndex } tc := cursorToolCallDelta{ Index: toolCallIndex, Function: &cursorToolCallFnDelta{Arguments: resp.Delta}, } return "", &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{ToolCalls: []cursorToolCallDelta{tc}}, }}, }, nil case schemas.ResponsesStreamResponseTypeOutputTextDelta: if resp.Delta == nil { return "", nil, nil } return "", &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{Content: resp.Delta}, }}, }, nil case schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta: if resp.Delta == nil { return "", nil, nil } return "", &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{Reasoning: resp.Delta}, }}, }, nil case schemas.ResponsesStreamResponseTypeRefusalDelta: // Map refusal to content so Cursor can display it if resp.Refusal == nil { return "", nil, nil } return "", &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{Content: resp.Refusal}, }}, }, nil case schemas.ResponsesStreamResponseTypeCompleted: finishReason := "stop" // If the response contains function call items, use "tool_calls" finish reason if resp.Response != nil { for _, item := range resp.Response.Output { if item.Type != nil && *item.Type == schemas.ResponsesMessageTypeFunctionCall { finishReason = "tool_calls" break } } } chunk := &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{}, FinishReason: &finishReason, }}, } // Include usage from the completed response if available if resp.Response != nil && resp.Response.Usage != nil { chunk.Usage = &cursorUsage{ PromptTokens: resp.Response.Usage.InputTokens, CompletionTokens: resp.Response.Usage.OutputTokens, TotalTokens: resp.Response.Usage.TotalTokens, } // Use response ID if available if resp.Response.ID != nil { chunk.ID = "chatcmpl-" + *resp.Response.ID } } return "", chunk, nil case schemas.ResponsesStreamResponseTypeFailed: finishReason := "stop" return "", &cursorChatChunk{ ID: cursorChunkID(&resp.ExtraFields), Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: cursorModel(&resp.ExtraFields), Choices: []cursorChatChoice{{ Index: 0, Delta: cursorChatDelta{}, FinishReason: &finishReason, }}, }, nil default: // Skip all other Responses API events (created, in_progress, content part events, etc.) return "", nil, nil } } // convertResponsesResponseToChatCompletion converts a non-streaming Responses API // response to a chat completion response object. func convertResponsesResponseToChatCompletion(resp *schemas.BifrostResponsesResponse) *cursorChatCompletion { // Extract text content and tool calls from output messages var sb strings.Builder var toolCalls []cursorToolCall finishReason := "stop" for _, msg := range resp.Output { // Function call items if msg.Type != nil && *msg.Type == schemas.ResponsesMessageTypeFunctionCall && msg.ResponsesToolMessage != nil { callID := "" if msg.CallID != nil { callID = *msg.CallID } name := "" if msg.Name != nil { name = *msg.Name } args := "" if msg.Arguments != nil { args = *msg.Arguments } toolCalls = append(toolCalls, cursorToolCall{ ID: callID, Type: "function", Function: cursorToolCallFn{ Name: name, Arguments: args, }, }) finishReason = "tool_calls" continue } // Text content if msg.Content == nil { continue } for _, block := range msg.Content.ContentBlocks { if block.Type == schemas.ResponsesOutputMessageContentTypeText && block.Text != nil { sb.WriteString(*block.Text) } } } content := sb.String() id := "chatcmpl-bifrost" if resp.ID != nil { id = "chatcmpl-" + *resp.ID } message := cursorChatCompletionMessage{ Role: "assistant", Content: content, } if len(toolCalls) > 0 { message.ToolCalls = toolCalls } result := &cursorChatCompletion{ ID: id, Object: "chat.completion", Created: int64(resp.CreatedAt), Model: resp.Model, Choices: []cursorChatCompletionChoice{{ Index: 0, Message: message, FinishReason: finishReason, }}, } if resp.Usage != nil { result.Usage = &cursorUsage{ PromptTokens: resp.Usage.InputTokens, CompletionTokens: resp.Usage.OutputTokens, TotalTokens: resp.Usage.TotalTokens, } } return result } // Cursor raw tool type // cursorRawTool represents the tool format Cursor actually sends: // // {"name":"Shell","description":"...","input_schema":{"type":"object",...}} // // This is neither Responses API format (which requires a "type" field) nor // Chat Completions format (which wraps in {"type":"function","function":{...}}). type cursorRawTool struct { Name string `json:"name"` Description string `json:"description"` InputSchema *schemas.ToolFunctionParameters `json:"input_schema,omitempty"` } // Cursor request parsing // cursorRequestParser handles Cursor's hybrid request format where input uses // Responses API format but tools may use chat completions format // ({"type":"function","function":{"name":"...","parameters":{...}}}). // // This is used as a RequestParser on the route config so it is explicitly called // by the router, avoiding reliance on sonic's UnmarshalJSON dispatch through interface{}. func cursorRequestParser(ctx *fasthttp.RequestCtx, req interface{}) error { cursorReq, ok := req.(*openai.OpenAIResponsesRequest) if !ok { return errors.New("invalid request type for cursor parser") } data := ctx.Request.Body() if len(data) == 0 { return nil } // Happy path: standard Responses API format (or no tools at all) if err := sonic.Unmarshal(data, cursorReq); err == nil { // If input is empty, Cursor may have sent "messages" (chat completions key) // instead of "input" (Responses API key). Convert messages → input. if len(cursorReq.Input.OpenAIResponsesRequestInputArray) == 0 && cursorReq.Input.OpenAIResponsesRequestInputStr == nil { cursorConvertMessagesToInput(data, cursorReq) } cursorConvertAnthropicToolBlocks(data, cursorReq) cursorMergeToolResultsFromMessages(data, cursorReq) normalizeInputContentBlocks(cursorReq) return nil } // Fallback: tools may be in Cursor's flat format (no "type" field, "input_schema" // instead of "parameters") which causes ResponsesTool.UnmarshalJSON to fail. // Parse all fields except tools using a tools-free struct to avoid triggering // ResponsesTool.UnmarshalJSON. type responsesParamsNoTools struct { schemas.ResponsesParameters Tools json.RawMessage `json:"tools,omitempty"` // shadow to absorb without parsing } var base struct { Model string `json:"model"` Input openai.OpenAIResponsesRequestInput `json:"input"` Stream *bool `json:"stream,omitempty"` Fallbacks []string `json:"fallbacks,omitempty"` responsesParamsNoTools } if err := sonic.Unmarshal(data, &base); err != nil { return err } cursorReq.Model = base.Model cursorReq.Input = base.Input cursorReq.Stream = base.Stream cursorReq.Fallbacks = base.Fallbacks cursorReq.ResponsesParameters = base.ResponsesParameters // If input is empty, Cursor may have sent "messages" instead of "input" if len(cursorReq.Input.OpenAIResponsesRequestInputArray) == 0 && cursorReq.Input.OpenAIResponsesRequestInputStr == nil { cursorConvertMessagesToInput(data, cursorReq) } cursorConvertAnthropicToolBlocks(data, cursorReq) cursorMergeToolResultsFromMessages(data, cursorReq) normalizeInputContentBlocks(cursorReq) // Parse tools from Cursor's flat format: // {"name":"Shell","description":"...","input_schema":{"type":"object","properties":{...},"required":[...]}} // This differs from both Responses API format (has "type" field) and Chat Completions format // (wraps in {"type":"function","function":{...}}). Cursor uses "input_schema" instead of "parameters". var toolsWrapper struct { Tools []cursorRawTool `json:"tools"` } if err := sonic.Unmarshal(data, &toolsWrapper); err != nil { return err } for i := range toolsWrapper.Tools { t := &toolsWrapper.Tools[i] name := t.Name desc := t.Description cursorReq.ResponsesParameters.Tools = append(cursorReq.ResponsesParameters.Tools, schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeFunction, Name: &name, Description: &desc, ResponsesToolFunction: &schemas.ResponsesToolFunction{ Parameters: t.InputSchema, }, }) } return nil } // cursorConvertAnthropicToolBlocks handles Cursor's Anthropic-style tool_use and tool_result // content blocks. Cursor can send these inside Responses API messages, but the standard // ResponsesMessageContentBlock struct doesn't have fields for tool_use_id or tool_result content, // so the data is lost during parsing. This function re-parses the raw JSON to extract tool blocks // and converts them to proper Responses API messages (function_call / function_call_output). func cursorConvertAnthropicToolBlocks(data []byte, cursorReq *openai.OpenAIResponsesRequest) { // Quick check: only process if there are tool_use or tool_result blocks in the raw JSON if !bytes.Contains(data, []byte("\"tool_use\"")) && !bytes.Contains(data, []byte("\"tool_result\"")) { return } // Re-parse from raw JSON to access tool block fields // Try both "input" (Responses API) and "messages" (chat completions) keys type rawMessage struct { Type *string `json:"type,omitempty"` Role *string `json:"role,omitempty"` ID *string `json:"id,omitempty"` Status *string `json:"status,omitempty"` Content json.RawMessage `json:"content,omitempty"` } var rawInput struct { Input []rawMessage `json:"input"` Messages []rawMessage `json:"messages"` } if err := sonic.Unmarshal(data, &rawInput); err != nil { return } // Use whichever array has content - prefer input, fallback to messages rawMessages := rawInput.Input if len(rawMessages) == 0 && len(rawInput.Messages) > 0 { rawMessages = rawInput.Messages } if len(rawMessages) == 0 { return } // Save the pre-converted input so we can reuse rich messages (with image blocks, // multi-part content, etc.) instead of falling back to createBasicMessage which // only preserves plain-string content. preConvertedInput := cursorReq.Input.OpenAIResponsesRequestInputArray type anthropicContentBlock struct { // Anthropic-style fields Type string `json:"type"` Text *string `json:"text,omitempty"` ID *string `json:"id,omitempty"` Name *string `json:"name,omitempty"` Input json.RawMessage `json:"input,omitempty"` ToolUseID *string `json:"tool_use_id,omitempty"` Content json.RawMessage `json:"content,omitempty"` // OpenAI-style fields (Cursor may use these instead of Anthropic-style) ToolCallID *string `json:"tool_call_id,omitempty"` Function *struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function,omitempty"` } // Helper to create a basic message from raw data when no tool blocks present createBasicMessage := func(rawMsg rawMessage) schemas.ResponsesMessage { msgType := schemas.ResponsesMessageTypeMessage var role schemas.ResponsesMessageRoleType if rawMsg.Role != nil { switch *rawMsg.Role { case "assistant": role = schemas.ResponsesInputMessageRoleAssistant case "system": role = schemas.ResponsesInputMessageRoleSystem default: role = schemas.ResponsesInputMessageRoleUser } } else { role = schemas.ResponsesInputMessageRoleUser } msg := schemas.ResponsesMessage{ Type: &msgType, Role: &role, } // Try to set content if rawMsg.Content != nil && len(rawMsg.Content) > 0 { // Try as string first var contentStr string if err := sonic.Unmarshal(rawMsg.Content, &contentStr); err == nil { msg.Content = &schemas.ResponsesMessageContent{ ContentStr: &contentStr, } } } return msg } var newInput []schemas.ResponsesMessage for i, rawMsg := range rawMessages { messageStart := len(newInput) if rawMsg.Content == nil || len(rawMsg.Content) == 0 { // Keep original message as-is if available, otherwise create basic message if i < len(preConvertedInput) { newInput = append(newInput, preConvertedInput[i]) } else { newInput = append(newInput, createBasicMessage(rawMsg)) } continue } // Try to parse content as array of blocks var blocks []anthropicContentBlock if err := sonic.Unmarshal(rawMsg.Content, &blocks); err != nil { // Content is a string or unparseable — keep original or create basic if i < len(preConvertedInput) { newInput = append(newInput, preConvertedInput[i]) } else { newInput = append(newInput, createBasicMessage(rawMsg)) } continue } hasToolBlocks := false for _, b := range blocks { if b.Type == "tool_use" || b.Type == "tool_result" { hasToolBlocks = true break } } if !hasToolBlocks { // No Anthropic tool blocks — keep original message or create basic if i < len(preConvertedInput) { newInput = append(newInput, preConvertedInput[i]) } else { newInput = append(newInput, createBasicMessage(rawMsg)) } continue } // Split message into regular content blocks and tool blocks var regularBlocks []schemas.ResponsesMessageContentBlock nextRegularIdx := 0 for _, b := range blocks { switch b.Type { case "tool_use": // Convert to function_call message // Support both Anthropic-style (id, name, input) and OpenAI-style (tool_call_id, function.name, function.arguments) callID := b.ID if callID == nil { callID = b.ToolCallID } toolName := b.Name var arguments *string if b.Function != nil { // OpenAI-style: function.name and function.arguments if toolName == nil { fnName := b.Function.Name toolName = &fnName } if b.Function.Arguments != "" { arguments = &b.Function.Arguments } } if arguments == nil && len(b.Input) > 0 && string(b.Input) != "null" { argStr := string(b.Input) arguments = &argStr } fcType := schemas.ResponsesMessageTypeFunctionCall newInput = append(newInput, schemas.ResponsesMessage{ ID: callID, Type: &fcType, Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant), Status: schemas.Ptr("completed"), ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: callID, Name: toolName, Arguments: arguments, }, }) case "tool_result": // Convert to function_call_output message // Support both Anthropic-style (tool_use_id) and OpenAI-style (tool_call_id) resultCallID := b.ToolUseID if resultCallID == nil { resultCallID = b.ToolCallID } fcoType := schemas.ResponsesMessageTypeFunctionCallOutput msg := schemas.ResponsesMessage{ Type: &fcoType, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: resultCallID, }, } // Extract output content if len(b.Content) > 0 && string(b.Content) != "null" { // Try as string first var contentStr string if err := sonic.Unmarshal(b.Content, &contentStr); err == nil { msg.ResponsesToolMessage.Output = &schemas.ResponsesToolMessageOutputStruct{ ResponsesToolCallOutputStr: &contentStr, } } else { // Try as array of content blocks var contentBlocks []struct { Type string `json:"type"` Text *string `json:"text,omitempty"` } if err := sonic.Unmarshal(b.Content, &contentBlocks); err == nil { var text strings.Builder for _, cb := range contentBlocks { if cb.Text != nil { text.WriteString(*cb.Text) } } if text.Len() > 0 { s := text.String() msg.ResponsesToolMessage.Output = &schemas.ResponsesToolMessageOutputStruct{ ResponsesToolCallOutputStr: &s, } } } } } newInput = append(newInput, msg) default: // Regular content block (text, image, etc.) — collect for the original message matched := false if i < len(preConvertedInput) { origMsg := preConvertedInput[i] if origMsg.Content != nil { for nextRegularIdx < len(origMsg.Content.ContentBlocks) { origBlock := origMsg.Content.ContentBlocks[nextRegularIdx] nextRegularIdx++ if string(origBlock.Type) == b.Type { regularBlocks = append(regularBlocks, origBlock) matched = true break } } } } // Fallback: create a text block if we have text if !matched && b.Text != nil { blockType := schemas.ResponsesInputMessageContentBlockTypeText if rawMsg.Role != nil && *rawMsg.Role == string(schemas.ResponsesInputMessageRoleAssistant) { blockType = schemas.ResponsesOutputMessageContentTypeText } regularBlocks = append(regularBlocks, schemas.ResponsesMessageContentBlock{ Type: blockType, Text: b.Text, }) } } } // If there were regular content blocks alongside tool blocks, add the original message with just those if len(regularBlocks) > 0 { var role *schemas.ResponsesMessageRoleType if rawMsg.Role != nil { r := schemas.ResponsesMessageRoleType(*rawMsg.Role) role = &r } msg := schemas.ResponsesMessage{ Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: regularBlocks, }, } if rawMsg.Type != nil { mt := schemas.ResponsesMessageType(*rawMsg.Type) msg.Type = &mt } // Insert the regular content message before the tool messages // Find the position where we started adding tool messages insertPos := len(newInput) for j := len(newInput) - 1; j >= messageStart; j-- { if newInput[j].Type != nil && (*newInput[j].Type == schemas.ResponsesMessageTypeFunctionCall || *newInput[j].Type == schemas.ResponsesMessageTypeFunctionCallOutput) { insertPos = j } else { break } } newInput = append(newInput[:insertPos], append([]schemas.ResponsesMessage{msg}, newInput[insertPos:]...)...) } } cursorReq.Input = openai.OpenAIResponsesRequestInput{ OpenAIResponsesRequestInputArray: newInput, } } // normalizeInputContentBlocks ensures all input messages have ContentBlocks instead of // ContentStr. Some providers (e.g. Anthropic) require content as an array of content blocks, // but Cursor may send content as a plain string. This must run after all parsing paths. func normalizeInputContentBlocks(req *openai.OpenAIResponsesRequest) { for i := range req.Input.OpenAIResponsesRequestInputArray { msg := &req.Input.OpenAIResponsesRequestInputArray[i] if msg.Content != nil && msg.Content.ContentStr != nil { text := msg.Content.ContentStr blockType := schemas.ResponsesInputMessageContentBlockTypeText if msg.Role != nil && *msg.Role == schemas.ResponsesInputMessageRoleAssistant { blockType = schemas.ResponsesOutputMessageContentTypeText } msg.Content = &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{{ Type: blockType, Text: text, }}, } } } } // isEffectivelyEmptyContent checks whether a message's content would produce an empty // content array after provider-level conversion. Cursor can send content blocks with // unrecognized types (e.g., "tool_result") that downstream converters filter out, // resulting in empty content. This uses a whitelist of known-good content types. func isEffectivelyEmptyContent(content *schemas.ResponsesMessageContent) bool { if content == nil { return true } if content.ContentStr != nil && strings.TrimSpace(*content.ContentStr) != "" { return false } if len(content.ContentBlocks) == 0 { return true } for _, block := range content.ContentBlocks { switch block.Type { case schemas.ResponsesInputMessageContentBlockTypeText, schemas.ResponsesOutputMessageContentTypeText: if block.Text != nil && strings.TrimSpace(*block.Text) != "" { return false } case schemas.ResponsesInputMessageContentBlockTypeImage: if block.ResponsesInputMessageContentBlockImage != nil { return false } case schemas.ResponsesInputMessageContentBlockTypeFile: if block.ResponsesInputMessageContentBlockFile != nil { return false } case schemas.ResponsesInputMessageContentBlockTypeAudio: if block.Audio != nil { return false } case schemas.ResponsesOutputMessageContentTypeCompaction: if block.ResponsesOutputMessageContentCompaction != nil { return false } // All other types (tool_result, unknown types, etc.) are considered // effectively empty since downstream converters will filter them out. } } return true } // normalizeBifrostInputContentBlocks ensures all input messages in a BifrostResponsesRequest // have ContentBlocks instead of ContentStr. This is a defense-in-depth normalization that runs // AFTER ToBifrostResponsesRequest, which can re-introduce ContentStr when the input is a string. func normalizeBifrostInputContentBlocks(req *schemas.BifrostResponsesRequest) { if req == nil { return } for i := range req.Input { msg := &req.Input[i] // Normalize message content: ContentStr → ContentBlocks if msg.Content != nil && msg.Content.ContentStr != nil { text := msg.Content.ContentStr blockType := schemas.ResponsesInputMessageContentBlockTypeText if msg.Role != nil && *msg.Role == schemas.ResponsesInputMessageRoleAssistant { blockType = schemas.ResponsesOutputMessageContentTypeText } msg.Content = &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{{ Type: blockType, Text: text, }}, } } // Cursor can send user messages with nil, empty, or effectively empty content // (e.g., text blocks with nil Text pointers that downstream providers filter out). // Anthropic requires user messages to have non-empty, non-whitespace content. // Backfill with a placeholder to prevent 400 errors. if msg.Role != nil && *msg.Role == schemas.ResponsesInputMessageRoleUser && isEffectivelyEmptyContent(msg.Content) { placeholder := "..." msg.Content = &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{{ Type: schemas.ResponsesInputMessageContentBlockTypeText, Text: &placeholder, }}, } } // Normalize tool output: ResponsesToolCallOutputStr → ResponsesFunctionToolCallOutputBlocks if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.Output != nil && msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr != nil && msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks == nil { text := msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks = []schemas.ResponsesMessageContentBlock{{ Type: schemas.ResponsesInputMessageContentBlockTypeText, Text: text, }} msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr = nil } } } // cursorMergeToolResultsFromMessages checks whether the parsed input is missing // function_call_output messages and, if so, looks for tool results in the "messages" // key (chat completions format with role:"tool"). When Cursor sends both "input" and // "messages", the input array may contain the conversation but omit tool results, // while the messages array has the complete conversation including role:"tool" entries. // In that case we replace input with the fully converted messages. func cursorMergeToolResultsFromMessages(data []byte, cursorReq *openai.OpenAIResponsesRequest) { // If we already have function_call_output messages, tool results are present for _, msg := range cursorReq.Input.OpenAIResponsesRequestInputArray { if msg.Type != nil && *msg.Type == schemas.ResponsesMessageTypeFunctionCallOutput { return } } // Quick check: does the raw body contain role:"tool" in messages? type msgProbeWithContent struct { Messages []struct { Role *string `json:"role"` Content json.RawMessage `json:"content"` } `json:"messages"` } var probe msgProbeWithContent if err := sonic.Unmarshal(data, &probe); err != nil || len(probe.Messages) == 0 { return } hasToolMessages := false for _, m := range probe.Messages { if m.Role != nil && *m.Role == "tool" { hasToolMessages = true break } // Cursor sends tool results as user messages with Anthropic-style tool_result content blocks if m.Role != nil && *m.Role == "user" && bytes.Contains(m.Content, []byte("\"tool_result\"")) { hasToolMessages = true break } } if !hasToolMessages { return } // Tool results exist in messages but not in input — use messages path instead. // This replaces input entirely with the properly converted messages array, // which includes function_call and function_call_output messages via ToResponsesMessages(). cursorConvertMessagesToInput(data, cursorReq) // Re-run Anthropic tool-block conversion on the newly replaced input. // ToResponsesMessages() doesn't handle Anthropic-style tool_result/tool_use content // blocks inside user messages, so we need cursorConvertAnthropicToolBlocks to // extract and convert them to proper function_call/function_call_output messages. cursorConvertAnthropicToolBlocks(data, cursorReq) } // cursorConvertMessagesToInput handles Cursor's use of "messages" (chat completions key) // instead of "input" (Responses API key). It parses chat completions messages and converts // them to Responses API input format using ChatMessage.ToResponsesMessages(). func cursorConvertMessagesToInput(data []byte, cursorReq *openai.OpenAIResponsesRequest) { var messagesWrapper struct { Messages []schemas.ChatMessage `json:"messages"` } if err := sonic.Unmarshal(data, &messagesWrapper); err != nil || len(messagesWrapper.Messages) == 0 { return } var allInput []schemas.ResponsesMessage for i := range messagesWrapper.Messages { allInput = append(allInput, messagesWrapper.Messages[i].ToResponsesMessages()...) } // Normalize ContentStr → ContentBlocks for all messages. // ToResponsesMessages() produces ContentStr (string) for user/system/developer messages, // but some providers (e.g. Anthropic) require content as an array of content blocks. for i := range allInput { if allInput[i].Content != nil && allInput[i].Content.ContentStr != nil { text := allInput[i].Content.ContentStr blockType := schemas.ResponsesInputMessageContentBlockTypeText if allInput[i].Role != nil && *allInput[i].Role == schemas.ResponsesInputMessageRoleAssistant { blockType = schemas.ResponsesOutputMessageContentTypeText } allInput[i].Content = &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{{ Type: blockType, Text: text, }}, } } } cursorReq.Input = openai.OpenAIResponsesRequestInput{ OpenAIResponsesRequestInputArray: allInput, } } // CursorRouter holds route registrations for Cursor IDE endpoints. // Cursor sends hybrid payloads using the OpenAI Responses API format // (input field with input_text content blocks) to chat completions endpoints. // This router routes /cursor/v1/chat/completions through the Responses API pipeline // while converting responses back to chat completions format that Cursor expects. type CursorRouter struct { *GenericRouter } // CreateCursorChatCompletionsRouteConfigs creates route configs for Cursor's chat completions endpoint. // It parses requests as OpenAI Responses API format since Cursor's payload is valid Responses API format, // but converts responses back to chat completions format (choices/delta/content). func CreateCursorChatCompletionsRouteConfigs(pathPrefix string, handlerStore lib.HandlerStore) []RouteConfig { routes := []RouteConfig{} for _, path := range []string{ "/v1/chat/completions", "/chat/completions", } { routes = append(routes, RouteConfig{ Type: RouteConfigTypeOpenAI, Path: pathPrefix + path, Method: "POST", GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType { return schemas.ResponsesRequest }, GetRequestTypeInstance: func(ctx context.Context) interface{} { return &openai.OpenAIResponsesRequest{} }, RequestParser: cursorRequestParser, RequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*schemas.BifrostRequest, error) { if openaiReq, ok := req.(*openai.OpenAIResponsesRequest); ok { bifrostReq := openaiReq.ToBifrostResponsesRequest(ctx) if bifrostReq == nil { return nil, errors.New("bifrost responses request conversion returned nil") } normalizeBifrostInputContentBlocks(bifrostReq) return &schemas.BifrostRequest{ ResponsesRequest: bifrostReq, }, nil } return nil, errors.New("invalid request type") }, ResponsesResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostResponsesResponse) (interface{}, error) { return convertResponsesResponseToChatCompletion(resp), nil }, ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} { return err }, StreamConfig: &StreamConfig{ ResponsesStreamResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostResponsesStreamResponse) (string, interface{}, error) { return convertResponsesStreamToChatChunk(resp) }, ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} { return err }, }, PreCallback: func(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error { // Set the user agent to cursor for tool manager duplicate checks bifrostCtx.SetValue(schemas.BifrostContextKeyUserAgent, schemas.Cursor.String()) return nil }, }) } return routes } // NewCursorRouter creates a new CursorRouter with the given bifrost client. func NewCursorRouter(client *bifrost.Bifrost, handlerStore lib.HandlerStore, logger schemas.Logger) *CursorRouter { routes := []RouteConfig{} // Custom Responses-based chat completions handler for Cursor's hybrid payloads routes = append(routes, CreateCursorChatCompletionsRouteConfigs("/cursor", handlerStore)...) // Add OpenAI list models route for /cursor/v1/models routes = append(routes, CreateOpenAIListModelsRouteConfigs("/cursor", handlerStore)...) // Add Anthropic routes for /cursor/anthropic/... routes = append(routes, CreateAnthropicRouteConfigs("/cursor", logger)...) // Add Anthropic count tokens route routes = append(routes, CreateAnthropicCountTokensRouteConfigs("/cursor", handlerStore)...) // Add GenAI routes for /cursor/genai/... routes = append(routes, CreateGenAIRouteConfigs("/cursor")...) // Add Bedrock routes for /cursor/bedrock/... routes = append(routes, CreateBedrockRouteConfigs("/cursor", handlerStore)...) // Add Cohere routes for /cursor/cohere/... routes = append(routes, CreateCohereRouteConfigs("/cursor")...) return &CursorRouter{ GenericRouter: NewGenericRouter(client, handlerStore, routes, nil, logger), } }