package openai import ( "encoding/json" "strings" "testing" "github.com/bytedance/sonic" providerUtils "github.com/maximhq/bifrost/core/providers/utils" "github.com/maximhq/bifrost/core/schemas" "github.com/stretchr/testify/require" ) func TestToOpenAIChatRequest_ToolNormalization(t *testing.T) { // Create tool parameters with keys in non-alphabetical order: // "required" before "properties" before "type" — Normalized() should reorder to // type → description → properties → required, then alphabetical. 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.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4o", Input: []schemas.ChatMessage{{Role: schemas.ChatMessageRoleUser}}, Params: &schemas.ChatParameters{ Tools: []schemas.ChatTool{ { Type: "function", Function: &schemas.ChatToolFunction{ Name: "test_func", Parameters: unsortedParams, }, }, { Type: "function", Function: &schemas.ChatToolFunction{Name: "no_params_func"}, }, }, }, } ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() result := ToOpenAIChatRequest(ctx, bifrostReq) if result == nil { t.Fatal("expected non-nil 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 := result.ChatParameters.Tools[0].Function.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 tool without parameters is unaffected if result.ChatParameters.Tools[1].Function.Parameters != nil { t.Error("expected nil parameters for tool without parameters") } // Verify original bifrostReq.Params.Tools was NOT mutated origKeys := bifrostReq.Params.Tools[0].Function.Parameters.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 Function pointer is a different object (deep copy) if result.ChatParameters.Tools[0].Function == bifrostReq.Params.Tools[0].Function { t.Error("expected Function pointer to be a copy, not the original") } } func TestToOpenAIChatRequest_PreservesN(t *testing.T) { req := &schemas.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4.1", Input: []schemas.ChatMessage{ { Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ ContentStr: schemas.Ptr("hello"), }, }, }, Params: &schemas.ChatParameters{ N: schemas.Ptr(2), }, } out := ToOpenAIChatRequest(schemas.NewBifrostContext(nil, schemas.NoDeadline), req) if out == nil { t.Fatal("expected request") } if out.N == nil || *out.N != 2 { t.Fatalf("expected n=2, got %#v", out.N) } } func TestToOpenAIChatRequest_PreservesPropertyOrder(t *testing.T) { params := &schemas.ToolFunctionParameters{ Type: "object", Properties: schemas.NewOrderedMapFromPairs( schemas.KV("reasoning", map[string]interface{}{"type": "string", "description": "Step by step"}), schemas.KV("answer", map[string]interface{}{"type": "string", "description": "Final answer"}), schemas.KV("confidence", map[string]interface{}{"type": "number", "description": "Score"}), ), Required: []string{"reasoning", "answer"}, } bifrostReq := &schemas.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4o", Input: []schemas.ChatMessage{{Role: schemas.ChatMessageRoleUser}}, Params: &schemas.ChatParameters{ Tools: []schemas.ChatTool{{ Type: "function", Function: &schemas.ChatToolFunction{Name: "test_func", Parameters: params}, }}, }, } ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() result := ToOpenAIChatRequest(ctx, bifrostReq) // CoT: property order preserved normalizedParams := result.ChatParameters.Tools[0].Function.Parameters keys := normalizedParams.Properties.Keys() if len(keys) != 3 || keys[0] != "reasoning" || keys[1] != "answer" || keys[2] != "confidence" { t.Errorf("expected property order [reasoning, answer, confidence], got %v", keys) } } func TestToOpenAIChatRequest_PreservesExplicitEmptyToolParameters(t *testing.T) { var tool schemas.ChatTool err := json.Unmarshal([]byte(`{"type":"function","function":{"name":"empty_schema","parameters":{},"strict":false}}`), &tool) if err != nil { t.Fatalf("failed to unmarshal tool: %v", err) } bifrostReq := &schemas.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4o", Input: []schemas.ChatMessage{{Role: schemas.ChatMessageRoleUser}}, Params: &schemas.ChatParameters{ Tools: []schemas.ChatTool{tool}, }, } ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() result := ToOpenAIChatRequest(ctx, bifrostReq) if result == nil { t.Fatal("expected non-nil result") } params := result.ChatParameters.Tools[0].Function.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) } } func TestToOpenAIChatRequest_CachingDeterminism(t *testing.T) { // Same properties, different structural key orders within property definitions makeReq := func(props *schemas.OrderedMap) *schemas.BifrostChatRequest { return &schemas.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4o", Input: []schemas.ChatMessage{{Role: schemas.ChatMessageRoleUser}}, Params: &schemas.ChatParameters{ Tools: []schemas.ChatTool{{ Type: "function", Function: &schemas.ChatToolFunction{ Name: "test", Parameters: &schemas.ToolFunctionParameters{Type: "object", Properties: props}, }, }}, }, } } // Version A: type before description propsA := schemas.NewOrderedMapFromPairs( schemas.KV("reasoning", schemas.NewOrderedMapFromPairs( schemas.KV("type", "string"), schemas.KV("description", "Step by step"), )), schemas.KV("answer", schemas.NewOrderedMapFromPairs( schemas.KV("type", "string"), schemas.KV("description", "Final answer"), )), ) // Version B: description before type (different structural order) propsB := schemas.NewOrderedMapFromPairs( schemas.KV("reasoning", schemas.NewOrderedMapFromPairs( schemas.KV("description", "Step by step"), schemas.KV("type", "string"), )), schemas.KV("answer", schemas.NewOrderedMapFromPairs( schemas.KV("description", "Final answer"), schemas.KV("type", "string"), )), ) ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() resultA := ToOpenAIChatRequest(ctx, makeReq(propsA)) resultB := ToOpenAIChatRequest(ctx, makeReq(propsB)) jsonA, err := schemas.Marshal(resultA.ChatParameters.Tools[0].Function.Parameters) if err != nil { t.Fatalf("failed to marshal params A: %v", err) } jsonB, err := schemas.Marshal(resultB.ChatParameters.Tools[0].Function.Parameters) if err != nil { t.Fatalf("failed to marshal params B: %v", err) } if string(jsonA) != string(jsonB) { t.Errorf("caching broken: same schema produced different JSON\nA: %s\nB: %s", jsonA, jsonB) } } func TestToOpenAIChatRequest_FireworksPreservesReasoningAndCacheIsolation(t *testing.T) { ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() cacheKey := "cache-key-1" reasoning := "step by step" predictionContent := "fireworks ok" userContent := "Reply with exactly: fireworks ok" bifrostReq := &schemas.BifrostChatRequest{ Provider: schemas.Fireworks, Model: "accounts/fireworks/models/deepseek-v3p2", Input: []schemas.ChatMessage{ { Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ ContentStr: &userContent, }, }, { Role: schemas.ChatMessageRoleAssistant, Content: &schemas.ChatMessageContent{ ContentStr: &predictionContent, }, ChatAssistantMessage: &schemas.ChatAssistantMessage{ Reasoning: &reasoning, }, }, }, Params: &schemas.ChatParameters{ PromptCacheKey: &cacheKey, Prediction: &schemas.ChatPrediction{ Type: "content", Content: predictionContent, }, }, } result := ToOpenAIChatRequest(ctx, bifrostReq) if result == nil { t.Fatal("expected non-nil result") } if result.PromptCacheIsolationKey == nil || *result.PromptCacheIsolationKey != cacheKey { t.Fatalf("expected prompt_cache_isolation_key %q, got %v", cacheKey, result.PromptCacheIsolationKey) } if result.PromptCacheKey != nil { t.Fatalf("expected prompt_cache_key to be stripped, got %v", *result.PromptCacheKey) } if result.Prediction == nil || result.Prediction.Content != predictionContent { t.Fatalf("expected prediction to be preserved, got %#v", result.Prediction) } if len(result.Messages) != 2 || result.Messages[1].OpenAIChatAssistantMessage == nil { t.Fatalf("expected assistant message with OpenAI assistant payload, got %#v", result.Messages) } if result.Messages[1].OpenAIChatAssistantMessage.Reasoning == nil || *result.Messages[1].OpenAIChatAssistantMessage.Reasoning != reasoning { t.Fatalf("expected assistant reasoning_content %q, got %#v", reasoning, result.Messages[1].OpenAIChatAssistantMessage) } ctx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true) wireBody, bifrostErr := providerUtils.CheckContextAndGetRequestBody( ctx, bifrostReq, func() (providerUtils.RequestBodyWithExtraParams, error) { return ToOpenAIChatRequest(ctx, bifrostReq), nil }, ) if bifrostErr != nil { t.Fatalf("failed to build request body: %v", bifrostErr.Error.Message) } var jsonMap map[string]interface{} if err := sonic.Unmarshal(wireBody, &jsonMap); err != nil { t.Fatalf("failed to parse marshaled request body: %v", err) } if got, ok := jsonMap["prompt_cache_isolation_key"].(string); !ok || got != cacheKey { t.Fatalf("expected prompt_cache_isolation_key %q in wire payload, got %#v", cacheKey, jsonMap["prompt_cache_isolation_key"]) } if _, ok := jsonMap["prompt_cache_key"]; ok { t.Fatalf("expected prompt_cache_key to be absent from wire payload, got %#v", jsonMap["prompt_cache_key"]) } messages, ok := jsonMap["messages"].([]interface{}) if !ok || len(messages) != 2 { t.Fatalf("expected 2 messages in wire payload, got %#v", jsonMap["messages"]) } assistantMessage, ok := messages[1].(map[string]interface{}) if !ok { t.Fatalf("expected assistant message object, got %#v", messages[1]) } if got, ok := assistantMessage["reasoning_content"].(string); !ok || got != reasoning { t.Fatalf("expected reasoning_content %q in assistant payload, got %#v", reasoning, assistantMessage["reasoning_content"]) } } // TestToOpenAIChatRequest_AnnotationsNotInWirePayload verifies that MCPToolAnnotations // (stored on ChatTool with json:"-") are never included in the JSON body sent to OpenAI. func TestToOpenAIChatRequest_AnnotationsNotInWirePayload(t *testing.T) { readOnly := true bifrostReq := &schemas.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4o", Input: []schemas.ChatMessage{ {Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("hello")}}, }, Params: &schemas.ChatParameters{ Tools: []schemas.ChatTool{ { Type: schemas.ChatToolTypeFunction, Function: &schemas.ChatToolFunction{ Name: "read_file", Description: schemas.Ptr("Read a file"), Parameters: &schemas.ToolFunctionParameters{ Type: "object", Properties: schemas.NewOrderedMapFromPairs( schemas.KV("path", map[string]interface{}{"type": "string"}), ), Required: []string{"path"}, }, }, Annotations: &schemas.MCPToolAnnotations{ Title: "File Reader", ReadOnlyHint: &readOnly, }, }, }, }, } ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() result := ToOpenAIChatRequest(ctx, bifrostReq) require.NotNil(t, result) wireBody, err := json.Marshal(result) require.NoError(t, err) s := string(wireBody) // Annotations must be absent from the wire payload if strings.Contains(s, "annotations") { t.Errorf("annotations field leaked into OpenAI wire payload: %s", s) } if strings.Contains(s, "readOnlyHint") { t.Errorf("readOnlyHint leaked into OpenAI wire payload: %s", s) } if strings.Contains(s, "File Reader") { t.Errorf("annotation title leaked into OpenAI wire payload: %s", s) } // The function definition must still be intact if !strings.Contains(s, "read_file") { t.Errorf("function name missing from OpenAI wire payload: %s", s) } } func TestApplyXAICompatibility(t *testing.T) { tests := []struct { name string model string request *OpenAIChatRequest validate func(t *testing.T, req *OpenAIChatRequest) }{ { name: "grok-3: preserves frequency_penalty and stop, clears presence_penalty and reasoning_effort", model: "grok-3", request: &OpenAIChatRequest{ Model: "grok-3", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ FrequencyPenalty: schemas.Ptr(0.5), PresencePenalty: schemas.Ptr(0.3), Stop: []string{"STOP"}, Reasoning: &schemas.ChatReasoning{ Effort: schemas.Ptr("high"), }, }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // frequency_penalty should be preserved if req.FrequencyPenalty == nil || *req.FrequencyPenalty != 0.5 { t.Errorf("Expected FrequencyPenalty to be preserved at 0.5, got %v", req.FrequencyPenalty) } // stop should be preserved if len(req.Stop) != 1 || req.Stop[0] != "STOP" { t.Errorf("Expected Stop to be preserved as ['STOP'], got %v", req.Stop) } // presence_penalty should be cleared if req.PresencePenalty != nil { t.Errorf("Expected PresencePenalty to be cleared (nil), got %v", *req.PresencePenalty) } // reasoning_effort should be cleared for non-mini grok-3 if req.Reasoning == nil { t.Fatal("Expected Reasoning to remain non-nil") } if req.Reasoning.Effort != nil { t.Errorf("Expected Reasoning.Effort to be cleared (nil) for grok-3, got %v", *req.Reasoning.Effort) } }, }, { name: "grok-3-mini: clears all penalties and stop, preserves reasoning_effort", model: "grok-3-mini", request: &OpenAIChatRequest{ Model: "grok-3-mini", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ FrequencyPenalty: schemas.Ptr(0.5), PresencePenalty: schemas.Ptr(0.3), Stop: []string{"STOP"}, Reasoning: &schemas.ChatReasoning{ Effort: schemas.Ptr("medium"), }, }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // presence_penalty should be cleared if req.PresencePenalty != nil { t.Errorf("Expected PresencePenalty to be cleared (nil), got %v", *req.PresencePenalty) } // frequency_penalty should be cleared for grok-3-mini if req.FrequencyPenalty != nil { t.Errorf("Expected FrequencyPenalty to be cleared (nil) for grok-3-mini, got %v", *req.FrequencyPenalty) } // stop should be cleared for grok-3-mini if req.Stop != nil { t.Errorf("Expected Stop to be cleared (nil) for grok-3-mini, got %v", req.Stop) } // reasoning_effort should be preserved for grok-3-mini if req.Reasoning == nil || req.Reasoning.Effort == nil { t.Fatal("Expected Reasoning.Effort to be preserved for grok-3-mini") } if *req.Reasoning.Effort != "medium" { t.Errorf("Expected Reasoning.Effort to be 'medium', got %v", *req.Reasoning.Effort) } }, }, { name: "grok-4: clears all penalties, stop, and reasoning_effort", model: "grok-4", request: &OpenAIChatRequest{ Model: "grok-4", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ FrequencyPenalty: schemas.Ptr(0.5), PresencePenalty: schemas.Ptr(0.3), Stop: []string{"STOP"}, Reasoning: &schemas.ChatReasoning{ Effort: schemas.Ptr("high"), }, }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // presence_penalty should be cleared if req.PresencePenalty != nil { t.Errorf("Expected PresencePenalty to be cleared (nil), got %v", *req.PresencePenalty) } // frequency_penalty should be cleared for grok-4 if req.FrequencyPenalty != nil { t.Errorf("Expected FrequencyPenalty to be cleared (nil) for grok-4, got %v", *req.FrequencyPenalty) } // stop should be cleared for grok-4 if req.Stop != nil { t.Errorf("Expected Stop to be cleared (nil) for grok-4, got %v", req.Stop) } // reasoning_effort should be cleared for grok-4 if req.Reasoning == nil { t.Fatal("Expected Reasoning to remain non-nil") } if req.Reasoning.Effort != nil { t.Errorf("Expected Reasoning.Effort to be cleared (nil) for grok-4, got %v", *req.Reasoning.Effort) } }, }, { name: "grok-4-fast-reasoning: clears all penalties, stop, and reasoning_effort", model: "grok-4-fast-reasoning", request: &OpenAIChatRequest{ Model: "grok-4-fast-reasoning", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ FrequencyPenalty: schemas.Ptr(0.5), PresencePenalty: schemas.Ptr(0.3), Stop: []string{"STOP", "END"}, Reasoning: &schemas.ChatReasoning{ Effort: schemas.Ptr("high"), }, }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // presence_penalty should be cleared if req.PresencePenalty != nil { t.Errorf("Expected PresencePenalty to be cleared (nil), got %v", *req.PresencePenalty) } // frequency_penalty should be cleared if req.FrequencyPenalty != nil { t.Errorf("Expected FrequencyPenalty to be cleared (nil), got %v", *req.FrequencyPenalty) } // stop should be cleared if req.Stop != nil { t.Errorf("Expected Stop to be cleared (nil), got %v", req.Stop) } // reasoning_effort should be cleared if req.Reasoning == nil { t.Fatal("Expected Reasoning to remain non-nil") } if req.Reasoning.Effort != nil { t.Errorf("Expected Reasoning.Effort to be cleared (nil), got %v", *req.Reasoning.Effort) } }, }, { name: "grok-code-fast-1: clears all penalties, stop, and reasoning_effort", model: "grok-code-fast-1", request: &OpenAIChatRequest{ Model: "grok-code-fast-1", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ FrequencyPenalty: schemas.Ptr(0.2), PresencePenalty: schemas.Ptr(0.1), Stop: []string{"END"}, Reasoning: &schemas.ChatReasoning{ Effort: schemas.Ptr("low"), }, }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // presence_penalty should be cleared if req.PresencePenalty != nil { t.Errorf("Expected PresencePenalty to be cleared (nil), got %v", *req.PresencePenalty) } // frequency_penalty should be cleared if req.FrequencyPenalty != nil { t.Errorf("Expected FrequencyPenalty to be cleared (nil), got %v", *req.FrequencyPenalty) } // stop should be cleared if req.Stop != nil { t.Errorf("Expected Stop to be cleared (nil), got %v", req.Stop) } // reasoning_effort should be cleared if req.Reasoning == nil { t.Fatal("Expected Reasoning to remain non-nil") } if req.Reasoning.Effort != nil { t.Errorf("Expected Reasoning.Effort to be cleared (nil), got %v", *req.Reasoning.Effort) } }, }, { name: "non-reasoning grok model: no changes applied", model: "grok-2-latest", request: &OpenAIChatRequest{ Model: "grok-2-latest", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ FrequencyPenalty: schemas.Ptr(0.5), PresencePenalty: schemas.Ptr(0.3), Stop: []string{"STOP"}, Reasoning: &schemas.ChatReasoning{ Effort: schemas.Ptr("high"), }, }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // All parameters should be preserved for non-reasoning models if req.FrequencyPenalty == nil || *req.FrequencyPenalty != 0.5 { t.Errorf("Expected FrequencyPenalty to be preserved at 0.5, got %v", req.FrequencyPenalty) } if req.PresencePenalty == nil || *req.PresencePenalty != 0.3 { t.Errorf("Expected PresencePenalty to be preserved at 0.3, got %v", req.PresencePenalty) } if len(req.Stop) != 1 || req.Stop[0] != "STOP" { t.Errorf("Expected Stop to be preserved as ['STOP'], got %v", req.Stop) } if req.Reasoning == nil || req.Reasoning.Effort == nil { t.Fatal("Expected Reasoning.Effort to be preserved") } if *req.Reasoning.Effort != "high" { t.Errorf("Expected Reasoning.Effort to be 'high', got %v", *req.Reasoning.Effort) } }, }, { name: "grok-3: handles nil reasoning gracefully", model: "grok-3", request: &OpenAIChatRequest{ Model: "grok-3", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ FrequencyPenalty: schemas.Ptr(0.5), PresencePenalty: schemas.Ptr(0.3), Stop: []string{"STOP"}, Reasoning: nil, }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // Should handle nil reasoning without panicking if req.Reasoning != nil { t.Errorf("Expected Reasoning to remain nil, got %v", req.Reasoning) } // Other parameters should still be processed if req.PresencePenalty != nil { t.Errorf("Expected PresencePenalty to be cleared (nil), got %v", *req.PresencePenalty) } if req.FrequencyPenalty == nil || *req.FrequencyPenalty != 0.5 { t.Errorf("Expected FrequencyPenalty to be preserved at 0.5, got %v", req.FrequencyPenalty) } }, }, { name: "grok-3: preserves other parameters like temperature", model: "grok-3", request: &OpenAIChatRequest{ Model: "grok-3", Messages: []OpenAIMessage{}, ChatParameters: schemas.ChatParameters{ Temperature: schemas.Ptr(0.8), TopP: schemas.Ptr(0.9), FrequencyPenalty: schemas.Ptr(0.5), PresencePenalty: schemas.Ptr(0.3), }, }, validate: func(t *testing.T, req *OpenAIChatRequest) { // Unrelated parameters should be preserved if req.Temperature == nil || *req.Temperature != 0.8 { t.Errorf("Expected Temperature to be preserved at 0.8, got %v", req.Temperature) } if req.TopP == nil || *req.TopP != 0.9 { t.Errorf("Expected TopP to be preserved at 0.9, got %v", req.TopP) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Apply the compatibility function tt.request.applyXAICompatibility(tt.model) // Validate the results tt.validate(t, tt.request) }) } }