Files
bifrost/core/providers/openai/responses_test.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

1599 lines
51 KiB
Go

package openai
import (
"encoding/json"
"strings"
"testing"
"github.com/maximhq/bifrost/core/schemas"
)
func TestToOpenAIResponsesRequest_ReasoningOnlyMessageSkip(t *testing.T) {
tests := []struct {
name string
model string
message schemas.ResponsesMessage
expectedIncluded bool
expectedEncryptedContent *string // if non-nil, assert converted message preserves this value
description string
}{
{
name: "reasoning-only message skipped for non-gpt-oss model",
model: "gpt-4o",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{}, // empty Summary
EncryptedContent: nil, // nil EncryptedContent
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: schemas.Ptr("reasoning text"),
},
}, // non-empty ContentBlocks
},
},
expectedIncluded: false,
description: "Message with ResponsesReasoning != nil, empty Summary, non-empty ContentBlocks, non-gpt-oss model, and nil EncryptedContent should be skipped",
},
{
name: "message with Summary preserved for non-gpt-oss model",
model: "gpt-4o",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{
{
Type: schemas.ResponsesReasoningContentBlockTypeSummaryText,
Text: "summary text",
},
}, // non-empty Summary
EncryptedContent: nil,
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: schemas.Ptr("reasoning text"),
},
},
},
},
expectedIncluded: true,
description: "Message with non-empty Summary should be preserved even if it has ContentBlocks",
},
{
name: "message with EncryptedContent skipped for non-reasoning model",
model: "gpt-4o",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{}, // empty Summary
EncryptedContent: schemas.Ptr("encrypted"), // non-nil EncryptedContent
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: schemas.Ptr("reasoning text"),
},
},
},
},
expectedIncluded: false,
description: "Non-reasoning models don't produce encrypted reasoning; cross-provider content should be skipped",
},
{
name: "message with EncryptedContent preserved for reasoning model",
model: "o3",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{}, // empty Summary
EncryptedContent: schemas.Ptr("encrypted"), // non-nil EncryptedContent
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: schemas.Ptr("reasoning text"),
},
},
},
},
expectedIncluded: true,
expectedEncryptedContent: schemas.Ptr("encrypted"),
description: "Reasoning models (o1/o3) produce encrypted content; should be preserved for multi-turn",
},
{
name: "message with empty ContentBlocks preserved for non-gpt-oss model",
model: "gpt-4o",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{}, // empty Summary
EncryptedContent: nil,
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{}, // empty ContentBlocks
},
},
expectedIncluded: true,
description: "Message with empty ContentBlocks should be preserved",
},
{
name: "message with nil Content preserved for non-gpt-oss model",
model: "gpt-4o",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{}, // empty Summary
EncryptedContent: nil,
},
Content: nil, // nil Content
},
expectedIncluded: true,
description: "Message with nil Content should be preserved",
},
{
name: "reasoning-only message preserved for gpt-oss model",
model: "gpt-oss",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{}, // empty Summary
EncryptedContent: nil,
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: schemas.Ptr("reasoning text"),
},
},
},
},
expectedIncluded: true,
description: "Message with reasoning-only content should be preserved for gpt-oss model",
},
{
name: "message without ResponsesReasoning preserved",
model: "gpt-4o",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser),
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: schemas.Ptr("regular text"),
},
},
},
},
expectedIncluded: true,
description: "Message without ResponsesReasoning should always be preserved",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bifrostReq := &schemas.BifrostResponsesRequest{
Model: tt.model,
Input: []schemas.ResponsesMessage{tt.message},
}
result := ToOpenAIResponsesRequest(bifrostReq)
if result == nil {
t.Fatal("ToOpenAIResponsesRequest returned nil")
}
messageCount := len(result.Input.OpenAIResponsesRequestInputArray)
isIncluded := messageCount > 0
if isIncluded != tt.expectedIncluded {
t.Errorf("%s: expected message to be included=%v (messageCount=%d), got included=%v (messageCount=%d)",
tt.description, tt.expectedIncluded, func() int {
if tt.expectedIncluded {
return 1
}
return 0
}(), isIncluded, messageCount)
}
// If message should be included, verify it's actually present
if tt.expectedIncluded && messageCount == 0 {
t.Error("Expected message to be included but result array is empty")
}
// If message should be excluded, verify it's not present
if !tt.expectedIncluded && messageCount > 0 {
t.Errorf("Expected message to be excluded but found %d message(s) in result", messageCount)
}
// If expectedEncryptedContent is set, verify the converted message preserves it
if tt.expectedEncryptedContent != nil && messageCount > 0 {
msg := result.Input.OpenAIResponsesRequestInputArray[0]
if msg.ResponsesReasoning == nil || msg.ResponsesReasoning.EncryptedContent == nil {
t.Error("Expected EncryptedContent to be preserved but ResponsesReasoning or EncryptedContent is nil")
} else if *msg.ResponsesReasoning.EncryptedContent != *tt.expectedEncryptedContent {
t.Errorf("Expected EncryptedContent=%q, got %q", *tt.expectedEncryptedContent, *msg.ResponsesReasoning.EncryptedContent)
}
}
})
}
}
func TestToOpenAIResponsesRequest_GPTOSS_SummaryToContentBlocks(t *testing.T) {
tests := []struct {
name string
model string
message schemas.ResponsesMessage
expectedBlocks int
expectedBlockText string
description string
}{
{
name: "gpt-oss converts Summary to ContentBlocks",
model: "gpt-oss",
message: schemas.ResponsesMessage{
ID: schemas.Ptr("msg-1"),
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
Status: schemas.Ptr("completed"),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{
{
Type: schemas.ResponsesReasoningContentBlockTypeSummaryText,
Text: "First summary",
},
{
Type: schemas.ResponsesReasoningContentBlockTypeSummaryText,
Text: "Second summary",
},
},
EncryptedContent: nil,
},
Content: nil, // No ContentBlocks initially
},
expectedBlocks: 2,
expectedBlockText: "First summary",
description: "gpt-oss model should convert Summary to ContentBlocks when Content is nil",
},
{
name: "gpt-oss preserves message when Content already exists",
model: "gpt-oss",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{
{
Type: schemas.ResponsesReasoningContentBlockTypeSummaryText,
Text: "summary text",
},
},
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: schemas.Ptr("existing content"),
},
},
},
},
expectedBlocks: 1,
expectedBlockText: "existing content",
description: "gpt-oss model should preserve message when Content already exists",
},
{
name: "gpt-oss variant model converts Summary to ContentBlocks",
model: "provider/gpt-oss-variant",
message: schemas.ResponsesMessage{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{
{
Type: schemas.ResponsesReasoningContentBlockTypeSummaryText,
Text: "variant summary",
},
},
},
Content: nil,
},
expectedBlocks: 1,
expectedBlockText: "variant summary",
description: "gpt-oss variant model should also convert Summary to ContentBlocks",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bifrostReq := &schemas.BifrostResponsesRequest{
Model: tt.model,
Input: []schemas.ResponsesMessage{tt.message},
}
result := ToOpenAIResponsesRequest(bifrostReq)
if result == nil {
t.Fatal("ToOpenAIResponsesRequest returned nil")
}
if len(result.Input.OpenAIResponsesRequestInputArray) != 1 {
t.Fatalf("Expected 1 message, got %d", len(result.Input.OpenAIResponsesRequestInputArray))
}
resultMsg := result.Input.OpenAIResponsesRequestInputArray[0]
// Check if Summary was converted to ContentBlocks for gpt-oss
if strings.Contains(tt.model, "gpt-oss") && len(tt.message.ResponsesReasoning.Summary) > 0 && tt.message.Content == nil {
if resultMsg.Content == nil {
t.Fatal("Expected Content to be created from Summary")
}
if len(resultMsg.Content.ContentBlocks) != tt.expectedBlocks {
t.Errorf("Expected %d ContentBlocks, got %d", tt.expectedBlocks, len(resultMsg.Content.ContentBlocks))
}
if len(resultMsg.Content.ContentBlocks) > 0 {
firstBlock := resultMsg.Content.ContentBlocks[0]
if firstBlock.Type != schemas.ResponsesOutputMessageContentTypeReasoning {
t.Errorf("Expected ContentBlock type to be reasoning_text, got %s", firstBlock.Type)
}
if firstBlock.Text == nil || *firstBlock.Text != tt.expectedBlockText {
t.Errorf("Expected first ContentBlock text to be %q, got %q", tt.expectedBlockText, func() string {
if firstBlock.Text == nil {
return "<nil>"
}
return *firstBlock.Text
}())
}
}
// Verify that original message fields are preserved
if tt.message.ID != nil && (resultMsg.ID == nil || *resultMsg.ID != *tt.message.ID) {
t.Errorf("Expected ID to be preserved")
}
if tt.message.Type != nil && (resultMsg.Type == nil || *resultMsg.Type != *tt.message.Type) {
t.Errorf("Expected Type to be preserved")
}
if tt.message.Status != nil && (resultMsg.Status == nil || *resultMsg.Status != *tt.message.Status) {
t.Errorf("Expected Status to be preserved")
}
if tt.message.Role != nil && (resultMsg.Role == nil || *resultMsg.Role != *tt.message.Role) {
t.Errorf("Expected Role to be preserved")
}
} else {
// For other cases, verify message is preserved as-is
if resultMsg.Content != nil && len(resultMsg.Content.ContentBlocks) > 0 {
if resultMsg.Content.ContentBlocks[0].Text == nil || *resultMsg.Content.ContentBlocks[0].Text != tt.expectedBlockText {
t.Errorf("Expected ContentBlock text to be preserved as %q", tt.expectedBlockText)
}
}
}
})
}
}
// =============================================================================
// ResponsesToolMessageActionStruct Marshal/Unmarshal Tests
// =============================================================================
func TestResponsesToolMessageActionStruct_MarshalUnmarshal_ComputerToolAction(t *testing.T) {
tests := []struct {
name string
action schemas.ResponsesToolMessageActionStruct
jsonData string
}{
{
name: "computer tool action - click",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesComputerToolCallAction: &schemas.ResponsesComputerToolCallAction{
Type: "click",
X: schemas.Ptr(100),
Y: schemas.Ptr(200),
},
},
jsonData: `{"type":"click","x":100,"y":200}`,
},
{
name: "computer tool action - screenshot",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesComputerToolCallAction: &schemas.ResponsesComputerToolCallAction{
Type: "screenshot",
},
},
jsonData: `{"type":"screenshot"}`,
},
{
name: "computer tool action - type with text",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesComputerToolCallAction: &schemas.ResponsesComputerToolCallAction{
Type: "type",
Text: schemas.Ptr("hello world"),
},
},
jsonData: `{"type":"type","text":"hello world"}`,
},
{
name: "computer tool action - scroll",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesComputerToolCallAction: &schemas.ResponsesComputerToolCallAction{
Type: "scroll",
ScrollX: schemas.Ptr(50),
ScrollY: schemas.Ptr(100),
},
},
jsonData: `{"type":"scroll","scroll_x":50,"scroll_y":100}`,
},
{
name: "computer tool action - zoom with region",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesComputerToolCallAction: &schemas.ResponsesComputerToolCallAction{
Type: "zoom",
Region: []int{0, 0, 1024, 768},
},
},
jsonData: `{"type":"zoom","region":[0,0,1024,768]}`,
},
}
for _, tt := range tests {
t.Run(tt.name+" - marshal", func(t *testing.T) {
data, err := json.Marshal(tt.action)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
// Unmarshal both to compare as maps (ignoring field order)
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(tt.jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", tt.jsonData, string(data))
}
})
t.Run(tt.name+" - unmarshal", func(t *testing.T) {
var action schemas.ResponsesToolMessageActionStruct
if err := json.Unmarshal([]byte(tt.jsonData), &action); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if action.ResponsesComputerToolCallAction == nil {
t.Fatal("expected ResponsesComputerToolCallAction to be populated")
}
if action.ResponsesComputerToolCallAction.Type != tt.action.ResponsesComputerToolCallAction.Type {
t.Errorf("type mismatch: expected %s, got %s",
tt.action.ResponsesComputerToolCallAction.Type,
action.ResponsesComputerToolCallAction.Type)
}
// Verify all other fields are nil (union type should have only one set)
if action.ResponsesWebSearchToolCallAction != nil {
t.Error("expected ResponsesWebSearchToolCallAction to be nil")
}
if action.ResponsesLocalShellToolCallAction != nil {
t.Error("expected ResponsesLocalShellToolCallAction to be nil")
}
if action.ResponsesMCPApprovalRequestAction != nil {
t.Error("expected ResponsesMCPApprovalRequestAction to be nil")
}
})
}
}
func TestResponsesToolMessageActionStruct_MarshalUnmarshal_WebSearchAction(t *testing.T) {
tests := []struct {
name string
action schemas.ResponsesToolMessageActionStruct
jsonData string
}{
{
name: "web search action - search",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesWebSearchToolCallAction: &schemas.ResponsesWebSearchToolCallAction{
Type: "search",
Query: schemas.Ptr("golang testing"),
},
},
jsonData: `{"type":"search","query":"golang testing"}`,
},
{
name: "web search action - open_page",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesWebSearchToolCallAction: &schemas.ResponsesWebSearchToolCallAction{
Type: "open_page",
URL: schemas.Ptr("https://example.com"),
},
},
jsonData: `{"type":"open_page","url":"https://example.com"}`,
},
{
name: "web search action - find",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesWebSearchToolCallAction: &schemas.ResponsesWebSearchToolCallAction{
Type: "find",
Pattern: schemas.Ptr("error.*occurred"),
},
},
jsonData: `{"type":"find","pattern":"error.*occurred"}`,
},
{
name: "web search action - search with queries array",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesWebSearchToolCallAction: &schemas.ResponsesWebSearchToolCallAction{
Type: "search",
Queries: []string{"query1", "query2"},
},
},
jsonData: `{"type":"search","queries":["query1","query2"]}`,
},
}
for _, tt := range tests {
t.Run(tt.name+" - marshal", func(t *testing.T) {
data, err := json.Marshal(tt.action)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(tt.jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", tt.jsonData, string(data))
}
})
t.Run(tt.name+" - unmarshal", func(t *testing.T) {
var action schemas.ResponsesToolMessageActionStruct
if err := json.Unmarshal([]byte(tt.jsonData), &action); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if action.ResponsesWebSearchToolCallAction == nil {
t.Fatal("expected ResponsesWebSearchToolCallAction to be populated")
}
if action.ResponsesWebSearchToolCallAction.Type != tt.action.ResponsesWebSearchToolCallAction.Type {
t.Errorf("type mismatch: expected %s, got %s",
tt.action.ResponsesWebSearchToolCallAction.Type,
action.ResponsesWebSearchToolCallAction.Type)
}
// Verify all other fields are nil
if action.ResponsesComputerToolCallAction != nil {
t.Error("expected ResponsesComputerToolCallAction to be nil")
}
if action.ResponsesLocalShellToolCallAction != nil {
t.Error("expected ResponsesLocalShellToolCallAction to be nil")
}
if action.ResponsesMCPApprovalRequestAction != nil {
t.Error("expected ResponsesMCPApprovalRequestAction to be nil")
}
})
}
}
func TestResponsesToolMessageActionStruct_MarshalUnmarshal_LocalShellAction(t *testing.T) {
tests := []struct {
name string
action schemas.ResponsesToolMessageActionStruct
jsonData string
}{
{
name: "local shell action - simple exec",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesLocalShellToolCallAction: &schemas.ResponsesLocalShellToolCallAction{
Type: "exec",
Command: []string{"ls", "-la"},
Env: []string{"PATH=/usr/bin"},
},
},
jsonData: `{"type":"exec","command":["ls","-la"],"env":["PATH=/usr/bin"]}`,
},
{
name: "local shell action - with timeout and working directory",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesLocalShellToolCallAction: &schemas.ResponsesLocalShellToolCallAction{
Type: "exec",
Command: []string{"npm", "test"},
Env: []string{},
TimeoutMS: schemas.Ptr(5000),
WorkingDirectory: schemas.Ptr("/home/user/project"),
},
},
jsonData: `{"type":"exec","command":["npm","test"],"env":[],"timeout_ms":5000,"working_directory":"/home/user/project"}`,
},
{
name: "local shell action - with user",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesLocalShellToolCallAction: &schemas.ResponsesLocalShellToolCallAction{
Type: "exec",
Command: []string{"whoami"},
Env: []string{},
User: schemas.Ptr("testuser"),
},
},
jsonData: `{"type":"exec","command":["whoami"],"env":[],"user":"testuser"}`,
},
}
for _, tt := range tests {
t.Run(tt.name+" - marshal", func(t *testing.T) {
data, err := json.Marshal(tt.action)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(tt.jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", tt.jsonData, string(data))
}
})
t.Run(tt.name+" - unmarshal", func(t *testing.T) {
var action schemas.ResponsesToolMessageActionStruct
if err := json.Unmarshal([]byte(tt.jsonData), &action); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if action.ResponsesLocalShellToolCallAction == nil {
t.Fatal("expected ResponsesLocalShellToolCallAction to be populated")
}
if action.ResponsesLocalShellToolCallAction.Type != "exec" {
t.Errorf("type mismatch: expected exec, got %s", action.ResponsesLocalShellToolCallAction.Type)
}
// Verify all other fields are nil
if action.ResponsesComputerToolCallAction != nil {
t.Error("expected ResponsesComputerToolCallAction to be nil")
}
if action.ResponsesWebSearchToolCallAction != nil {
t.Error("expected ResponsesWebSearchToolCallAction to be nil")
}
if action.ResponsesMCPApprovalRequestAction != nil {
t.Error("expected ResponsesMCPApprovalRequestAction to be nil")
}
})
}
}
func TestResponsesToolMessageActionStruct_MarshalUnmarshal_MCPApprovalAction(t *testing.T) {
tests := []struct {
name string
action schemas.ResponsesToolMessageActionStruct
jsonData string
}{
{
name: "mcp approval request action",
action: schemas.ResponsesToolMessageActionStruct{
ResponsesMCPApprovalRequestAction: &schemas.ResponsesMCPApprovalRequestAction{
ID: "approval-123",
Type: "mcp_approval_request",
Name: "test_tool",
ServerLabel: "test-server",
Arguments: `{"key":"value"}`,
},
},
jsonData: `{"id":"approval-123","type":"mcp_approval_request","name":"test_tool","server_label":"test-server","arguments":"{\"key\":\"value\"}"}`,
},
}
for _, tt := range tests {
t.Run(tt.name+" - marshal", func(t *testing.T) {
data, err := json.Marshal(tt.action)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(tt.jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", tt.jsonData, string(data))
}
})
t.Run(tt.name+" - unmarshal", func(t *testing.T) {
var action schemas.ResponsesToolMessageActionStruct
if err := json.Unmarshal([]byte(tt.jsonData), &action); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if action.ResponsesMCPApprovalRequestAction == nil {
t.Fatal("expected ResponsesMCPApprovalRequestAction to be populated")
}
if action.ResponsesMCPApprovalRequestAction.Type != "mcp_approval_request" {
t.Errorf("type mismatch: expected mcp_approval_request, got %s", action.ResponsesMCPApprovalRequestAction.Type)
}
// Verify all other fields are nil
if action.ResponsesComputerToolCallAction != nil {
t.Error("expected ResponsesComputerToolCallAction to be nil")
}
if action.ResponsesWebSearchToolCallAction != nil {
t.Error("expected ResponsesWebSearchToolCallAction to be nil")
}
if action.ResponsesLocalShellToolCallAction != nil {
t.Error("expected ResponsesLocalShellToolCallAction to be nil")
}
})
}
}
func TestResponsesToolMessageActionStruct_EdgeCases(t *testing.T) {
t.Run("empty action struct - marshal should error", func(t *testing.T) {
action := schemas.ResponsesToolMessageActionStruct{}
_, err := json.Marshal(action)
if err == nil {
t.Error("expected error when marshaling empty action struct")
}
})
t.Run("unknown action type - unmarshal to computer tool (default)", func(t *testing.T) {
jsonData := `{"type":"unknown_action"}`
var action schemas.ResponsesToolMessageActionStruct
if err := json.Unmarshal([]byte(jsonData), &action); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
// Default behavior is to unmarshal to computer tool
if action.ResponsesComputerToolCallAction == nil {
t.Error("expected ResponsesComputerToolCallAction to be populated for unknown type")
}
})
t.Run("round trip - computer action", func(t *testing.T) {
original := schemas.ResponsesToolMessageActionStruct{
ResponsesComputerToolCallAction: &schemas.ResponsesComputerToolCallAction{
Type: "click",
X: schemas.Ptr(150),
Y: schemas.Ptr(250),
},
}
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
var unmarshaled schemas.ResponsesToolMessageActionStruct
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
if unmarshaled.ResponsesComputerToolCallAction == nil {
t.Fatal("expected ResponsesComputerToolCallAction to be populated")
}
if unmarshaled.ResponsesComputerToolCallAction.Type != "click" {
t.Errorf("type mismatch: expected click, got %s", unmarshaled.ResponsesComputerToolCallAction.Type)
}
if unmarshaled.ResponsesComputerToolCallAction.X == nil || *unmarshaled.ResponsesComputerToolCallAction.X != 150 {
t.Errorf("X coordinate mismatch")
}
})
}
// =============================================================================
// ResponsesTool Marshal/Unmarshal Tests
// =============================================================================
func TestResponsesTool_MarshalUnmarshal_FunctionTool(t *testing.T) {
tests := []struct {
name string
tool schemas.ResponsesTool
jsonData string
}{
{
name: "function tool with name and description",
tool: schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeFunction,
Name: schemas.Ptr("get_weather"),
Description: schemas.Ptr("Get the current weather"),
ResponsesToolFunction: &schemas.ResponsesToolFunction{
Strict: schemas.Ptr(true),
},
},
jsonData: `{"type":"function","name":"get_weather","description":"Get the current weather","strict":true}`,
},
{
name: "function tool with cache control",
tool: schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeFunction,
Name: schemas.Ptr("search_db"),
Description: schemas.Ptr("Search database"),
CacheControl: &schemas.CacheControl{
Type: "ephemeral",
},
ResponsesToolFunction: &schemas.ResponsesToolFunction{
Strict: schemas.Ptr(false),
},
},
jsonData: `{"type":"function","name":"search_db","description":"Search database","cache_control":{"type":"ephemeral"},"strict":false}`,
},
}
for _, tt := range tests {
t.Run(tt.name+" - marshal", func(t *testing.T) {
data, err := json.Marshal(tt.tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(tt.jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", tt.jsonData, string(data))
}
})
t.Run(tt.name+" - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(tt.jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeFunction {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeFunction, tool.Type)
}
if tool.ResponsesToolFunction == nil {
t.Fatal("expected ResponsesToolFunction to be populated")
}
if tool.Name == nil || *tool.Name != *tt.tool.Name {
t.Error("name mismatch")
}
if tool.Description == nil || *tool.Description != *tt.tool.Description {
t.Error("description mismatch")
}
})
}
}
func TestResponsesTool_MarshalUnmarshal_FileSearchTool(t *testing.T) {
jsonData := `{"type":"file_search","vector_store_ids":null}`
t.Run("file search tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeFileSearch,
ResponsesToolFileSearch: &schemas.ResponsesToolFileSearch{},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("file search tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeFileSearch {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeFileSearch, tool.Type)
}
if tool.ResponsesToolFileSearch == nil {
t.Fatal("expected ResponsesToolFileSearch to be populated")
}
})
}
func TestResponsesTool_MarshalUnmarshal_ComputerUseTool(t *testing.T) {
jsonData := `{"type":"computer_use_preview","display_height":1080,"display_width":1920,"environment":"browser"}`
t.Run("computer use preview tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeComputerUsePreview,
ResponsesToolComputerUsePreview: &schemas.ResponsesToolComputerUsePreview{
DisplayWidth: 1920,
DisplayHeight: 1080,
Environment: "browser",
},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("computer use preview tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeComputerUsePreview {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeComputerUsePreview, tool.Type)
}
if tool.ResponsesToolComputerUsePreview == nil {
t.Fatal("expected ResponsesToolComputerUsePreview to be populated")
}
})
}
func TestResponsesTool_MarshalUnmarshal_WebSearchTool(t *testing.T) {
jsonData := `{"type":"web_search","search_context_size":"medium"}`
t.Run("web search tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeWebSearch,
ResponsesToolWebSearch: &schemas.ResponsesToolWebSearch{
SearchContextSize: schemas.Ptr("medium"),
},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("web search tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeWebSearch {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeWebSearch, tool.Type)
}
if tool.ResponsesToolWebSearch == nil {
t.Fatal("expected ResponsesToolWebSearch to be populated")
}
if tool.ResponsesToolWebSearch.SearchContextSize == nil || *tool.ResponsesToolWebSearch.SearchContextSize != "medium" {
t.Error("search_context_size mismatch")
}
})
}
func TestResponsesTool_MarshalUnmarshal_MCPTool(t *testing.T) {
jsonData := `{"type":"mcp","name":"test_mcp_tool","server_label":"mcp-server-1"}`
t.Run("mcp tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeMCP,
Name: schemas.Ptr("test_mcp_tool"),
ResponsesToolMCP: &schemas.ResponsesToolMCP{
ServerLabel: "mcp-server-1",
},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("mcp tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeMCP {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeMCP, tool.Type)
}
if tool.ResponsesToolMCP == nil {
t.Fatal("expected ResponsesToolMCP to be populated")
}
if tool.ResponsesToolMCP.ServerLabel != "mcp-server-1" {
t.Error("server_label mismatch")
}
})
}
func TestResponsesTool_MarshalUnmarshal_CodeInterpreterTool(t *testing.T) {
jsonData := `{"type":"code_interpreter","container":null}`
t.Run("code interpreter tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeCodeInterpreter,
ResponsesToolCodeInterpreter: &schemas.ResponsesToolCodeInterpreter{},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("code interpreter tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeCodeInterpreter {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeCodeInterpreter, tool.Type)
}
if tool.ResponsesToolCodeInterpreter == nil {
t.Fatal("expected ResponsesToolCodeInterpreter to be populated")
}
})
}
func TestResponsesTool_MarshalUnmarshal_ImageGenerationTool(t *testing.T) {
jsonData := `{"type":"image_generation"}`
t.Run("image generation tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeImageGeneration,
ResponsesToolImageGeneration: &schemas.ResponsesToolImageGeneration{},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("image generation tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeImageGeneration {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeImageGeneration, tool.Type)
}
if tool.ResponsesToolImageGeneration == nil {
t.Fatal("expected ResponsesToolImageGeneration to be populated")
}
})
}
func TestResponsesTool_MarshalUnmarshal_LocalShellTool(t *testing.T) {
jsonData := `{"type":"local_shell"}`
t.Run("local shell tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeLocalShell,
ResponsesToolLocalShell: &schemas.ResponsesToolLocalShell{},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("local shell tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeLocalShell {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeLocalShell, tool.Type)
}
if tool.ResponsesToolLocalShell == nil {
t.Fatal("expected ResponsesToolLocalShell to be populated")
}
})
}
func TestResponsesTool_MarshalUnmarshal_CustomTool(t *testing.T) {
jsonData := `{"type":"custom","name":"custom_tool","description":"A custom tool"}`
t.Run("custom tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeCustom,
Name: schemas.Ptr("custom_tool"),
Description: schemas.Ptr("A custom tool"),
ResponsesToolCustom: &schemas.ResponsesToolCustom{},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("custom tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeCustom {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeCustom, tool.Type)
}
if tool.ResponsesToolCustom == nil {
t.Fatal("expected ResponsesToolCustom to be populated")
}
if tool.Name == nil || *tool.Name != "custom_tool" {
t.Error("name mismatch")
}
if tool.Description == nil || *tool.Description != "A custom tool" {
t.Error("description mismatch")
}
})
}
func TestResponsesTool_MarshalUnmarshal_WebSearchPreviewTool(t *testing.T) {
jsonData := `{"type":"web_search_preview","search_context_size":"high"}`
t.Run("web search preview tool - marshal", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeWebSearchPreview,
ResponsesToolWebSearchPreview: &schemas.ResponsesToolWebSearchPreview{
SearchContextSize: schemas.Ptr("high"),
},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(jsonData), &expected); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !mapsEqual(expected, actual) {
t.Errorf("marshaled JSON mismatch\nexpected: %s\nactual: %s", jsonData, string(data))
}
})
t.Run("web search preview tool - unmarshal", func(t *testing.T) {
var tool schemas.ResponsesTool
if err := json.Unmarshal([]byte(jsonData), &tool); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if tool.Type != schemas.ResponsesToolTypeWebSearchPreview {
t.Errorf("type mismatch: expected %s, got %s", schemas.ResponsesToolTypeWebSearchPreview, tool.Type)
}
if tool.ResponsesToolWebSearchPreview == nil {
t.Fatal("expected ResponsesToolWebSearchPreview to be populated")
}
})
}
func TestResponsesTool_EdgeCases(t *testing.T) {
t.Run("missing type field - unmarshal should error", func(t *testing.T) {
jsonData := `{"name":"test"}`
var tool schemas.ResponsesTool
err := json.Unmarshal([]byte(jsonData), &tool)
if err == nil {
t.Error("expected error when unmarshaling tool without type field")
}
})
t.Run("round trip - function tool with all fields", func(t *testing.T) {
original := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeFunction,
Name: schemas.Ptr("get_weather"),
Description: schemas.Ptr("Get weather info"),
CacheControl: &schemas.CacheControl{
Type: "ephemeral",
},
ResponsesToolFunction: &schemas.ResponsesToolFunction{
Strict: schemas.Ptr(true),
},
}
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
var unmarshaled schemas.ResponsesTool
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
if unmarshaled.Type != schemas.ResponsesToolTypeFunction {
t.Error("type mismatch")
}
if unmarshaled.Name == nil || *unmarshaled.Name != "get_weather" {
t.Error("name mismatch")
}
if unmarshaled.Description == nil || *unmarshaled.Description != "Get weather info" {
t.Error("description mismatch")
}
if unmarshaled.CacheControl == nil || unmarshaled.CacheControl.Type != "ephemeral" {
t.Error("cache_control mismatch")
}
if unmarshaled.ResponsesToolFunction == nil || unmarshaled.ResponsesToolFunction.Strict == nil || !*unmarshaled.ResponsesToolFunction.Strict {
t.Error("strict field mismatch")
}
})
t.Run("round trip - web search tool with user location", func(t *testing.T) {
original := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeWebSearch,
ResponsesToolWebSearch: &schemas.ResponsesToolWebSearch{
SearchContextSize: schemas.Ptr("medium"),
UserLocation: &schemas.ResponsesToolWebSearchUserLocation{
City: schemas.Ptr("San Francisco"),
Country: schemas.Ptr("US"),
Timezone: schemas.Ptr("America/Los_Angeles"),
},
},
}
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
var unmarshaled schemas.ResponsesTool
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
if unmarshaled.ResponsesToolWebSearch == nil {
t.Fatal("expected ResponsesToolWebSearch to be populated")
}
if unmarshaled.ResponsesToolWebSearch.UserLocation == nil {
t.Fatal("expected UserLocation to be populated")
}
if unmarshaled.ResponsesToolWebSearch.UserLocation.City == nil || *unmarshaled.ResponsesToolWebSearch.UserLocation.City != "San Francisco" {
t.Error("city mismatch")
}
})
t.Run("nil embedded struct - should marshal type only", func(t *testing.T) {
tool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeFunction,
Name: schemas.Ptr("test"),
// ResponsesToolFunction is nil
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("failed to unmarshal result: %v", err)
}
if result["type"] != "function" {
t.Error("type mismatch")
}
if result["name"] != "test" {
t.Error("name mismatch")
}
})
}
func TestToOpenAIResponsesRequest_ToolNormalization(t *testing.T) {
// Create function tool with unsorted properties
unsortedParams := &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMapFromPairs(
schemas.KV("zebra", map[string]interface{}{"type": "string"}),
schemas.KV("alpha", map[string]interface{}{"type": "number"}),
),
Required: []string{"zebra"},
}
bifrostReq := &schemas.BifrostResponsesRequest{
Provider: schemas.OpenAI,
Model: "gpt-4o",
Input: []schemas.ResponsesMessage{
{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser),
Content: &schemas.ResponsesMessageContent{
ContentStr: schemas.Ptr("hello"),
},
},
},
Params: &schemas.ResponsesParameters{
Tools: []schemas.ResponsesTool{
{
Type: schemas.ResponsesToolTypeFunction,
Name: schemas.Ptr("test_func"),
ResponsesToolFunction: &schemas.ResponsesToolFunction{
Parameters: unsortedParams,
},
},
{
Type: schemas.ResponsesToolTypeWebSearch,
ResponsesToolWebSearch: &schemas.ResponsesToolWebSearch{},
},
},
},
}
result := ToOpenAIResponsesRequest(bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
// Find the function tool in the result (filterUnsupportedTools may reorder)
var funcTool *schemas.ResponsesTool
var nonFuncToolCount int
for i := range result.Tools {
if result.Tools[i].Type == schemas.ResponsesToolTypeFunction {
funcTool = &result.Tools[i]
} else {
nonFuncToolCount++
}
}
if funcTool == nil {
t.Fatal("expected function tool in result")
}
// Verify parameters are normalized: Properties keys should preserve original order
// (user-defined property names are kept in client order for LLM generation quality)
normalizedParams := funcTool.ResponsesToolFunction.Parameters
if normalizedParams == nil {
t.Fatal("expected normalized parameters to be non-nil")
}
keys := normalizedParams.Properties.Keys()
if len(keys) != 2 || keys[0] != "zebra" || keys[1] != "alpha" {
t.Errorf("expected Properties keys preserved as [zebra, alpha], got %v", keys)
}
// Verify non-function tools are present and unaffected
if nonFuncToolCount != 1 {
t.Errorf("expected 1 non-function tool, got %d", nonFuncToolCount)
}
// Verify original bifrostReq.Params.Tools was NOT mutated
origParams := bifrostReq.Params.Tools[0].ResponsesToolFunction.Parameters
origKeys := origParams.Properties.Keys()
if len(origKeys) != 2 || origKeys[0] != "zebra" || origKeys[1] != "alpha" {
t.Errorf("original parameters were mutated: expected [zebra, alpha], got %v", origKeys)
}
// Verify the ResponsesToolFunction pointer is a different object
if funcTool.ResponsesToolFunction == bifrostReq.Params.Tools[0].ResponsesToolFunction {
t.Error("expected ResponsesToolFunction pointer to be a copy, not the original")
}
}
func TestToOpenAIResponsesRequest_PreservesExplicitEmptyToolParameters(t *testing.T) {
var tool schemas.ResponsesTool
err := json.Unmarshal([]byte(`{"type":"function","name":"empty_schema","parameters":{},"strict":false}`), &tool)
if err != nil {
t.Fatalf("failed to unmarshal tool: %v", err)
}
bifrostReq := &schemas.BifrostResponsesRequest{
Provider: schemas.OpenAI,
Model: "gpt-4o",
Input: []schemas.ResponsesMessage{
{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser),
Content: &schemas.ResponsesMessageContent{
ContentStr: schemas.Ptr("hello"),
},
},
},
Params: &schemas.ResponsesParameters{
Tools: []schemas.ResponsesTool{tool},
},
}
result := ToOpenAIResponsesRequest(bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
params := result.Tools[0].ResponsesToolFunction.Parameters
if params == nil {
t.Fatal("expected tool parameters to be preserved")
}
marshaled, err := schemas.Marshal(params)
if err != nil {
t.Fatalf("failed to marshal parameters: %v", err)
}
if string(marshaled) != `{}` {
t.Fatalf("expected parameters to remain {}, got %s", marshaled)
}
}
// =============================================================================
// Helper Functions
// =============================================================================
// mapsEqual compares two maps for equality (including nested maps and arrays)
func mapsEqual(a, b map[string]interface{}) bool {
if len(a) != len(b) {
return false
}
for k, v1 := range a {
v2, ok := b[k]
if !ok {
return false
}
if !valuesEqual(v1, v2) {
return false
}
}
return true
}
// valuesEqual compares two values for equality (handles nested structures)
func valuesEqual(v1, v2 interface{}) bool {
switch val1 := v1.(type) {
case map[string]interface{}:
val2, ok := v2.(map[string]interface{})
if !ok {
return false
}
return mapsEqual(val1, val2)
case []interface{}:
val2, ok := v2.([]interface{})
if !ok {
return false
}
if len(val1) != len(val2) {
return false
}
for i := range val1 {
if !valuesEqual(val1[i], val2[i]) {
return false
}
}
return true
default:
// For primitives, use direct comparison
return v1 == v2
}
}