first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
package azure_test
import (
"strings"
"testing"
"github.com/maximhq/bifrost/core/providers/openai"
"github.com/maximhq/bifrost/core/schemas"
)
// TestAzure_OpenAIModel_CachingDeterminism verifies that Azure's delegation to
// openai.ToOpenAIChatRequest() produces deterministic JSON for prompt caching.
// Two schemas with the same properties but different structural key order within
// property definitions must produce byte-identical JSON after normalization.
func TestAzure_OpenAIModel_CachingDeterminism(t *testing.T) {
makeReq := func(props *schemas.OrderedMap) *schemas.BifrostChatRequest {
return &schemas.BifrostChatRequest{
Provider: schemas.Azure,
Model: "gpt-4o",
Input: []schemas.ChatMessage{{Role: schemas.ChatMessageRoleUser}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
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"),
)),
)
// Azure delegates OpenAI models to openai.ToOpenAIChatRequest()
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
resultA := openai.ToOpenAIChatRequest(ctx, makeReq(propsA))
resultB := openai.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)
}
// Caching: byte-identical JSON
if string(jsonA) != string(jsonB) {
t.Errorf("caching broken via Azure→OpenAI path: same schema produced different JSON\nA: %s\nB: %s", jsonA, jsonB)
}
// CoT: property order preserved
keys := resultA.ChatParameters.Tools[0].Function.Parameters.Properties.Keys()
if len(keys) != 2 || keys[0] != "reasoning" || keys[1] != "answer" {
t.Errorf("expected property order [reasoning, answer], got %v", keys)
}
}
// TestAzure_OpenAIModel_PreservesPropertyOrder verifies that the Azure→OpenAI
// delegation path preserves user-defined property ordering.
func TestAzure_OpenAIModel_PreservesPropertyOrder(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Azure,
Model: "gpt-4o",
Input: []schemas.ChatMessage{{Role: schemas.ChatMessageRoleUser}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "AnswerResponseModel",
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMapFromPairs(
schemas.KV("chain_of_thought", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))),
schemas.KV("answer", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))),
schemas.KV("citations", schemas.NewOrderedMapFromPairs(schemas.KV("type", "array"))),
),
},
},
}},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result := openai.ToOpenAIChatRequest(ctx, bifrostReq)
keys := result.ChatParameters.Tools[0].Function.Parameters.Properties.Keys()
if len(keys) != 3 || keys[0] != "chain_of_thought" || keys[1] != "answer" || keys[2] != "citations" {
t.Errorf("expected property order [chain_of_thought, answer, citations], got %v", keys)
}
}
// TestAzure_ToolInputKeyOrderPreservation verifies that tool call arguments
// preserve their original key ordering through the Azure→OpenAI delegation path.
// TestAzure_ToolInputKeyOrderPreservation verifies that Azure→OpenAI delegation
// preserves the original key ordering of tool call arguments for prompt caching.
// Tests multiple parallel tool calls with different key orderings per block.
func TestAzure_ToolInputKeyOrderPreservation(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Azure,
Model: "gpt-4o",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")},
},
{
Role: schemas.ChatMessageRoleAssistant,
ChatAssistantMessage: &schemas.ChatAssistantMessage{
ToolCalls: []schemas.ChatAssistantMessageToolCall{
{
Index: 0,
Type: schemas.Ptr("function"),
ID: schemas.Ptr("toolu_001"),
Function: schemas.ChatAssistantMessageToolCallFunction{
Name: schemas.Ptr("bash"),
Arguments: `{"description":"Find references quickly","timeout":30000,"command":"grep -r auth_injector ."}`,
},
},
{
Index: 1,
Type: schemas.Ptr("function"),
ID: schemas.Ptr("toolu_002"),
Function: schemas.ChatAssistantMessageToolCallFunction{
Name: schemas.Ptr("bash"),
Arguments: `{"command":"git diff main...HEAD --stat","description":"Show diff"}`,
},
},
},
},
},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result := openai.ToOpenAIChatRequest(ctx, bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
// Collect tool call arguments from assistant message
var argsList []string
for _, msg := range result.Messages {
if msg.OpenAIChatAssistantMessage != nil {
for _, tc := range msg.OpenAIChatAssistantMessage.ToolCalls {
argsList = append(argsList, tc.Function.Arguments)
}
}
}
if len(argsList) != 2 {
t.Fatalf("expected 2 tool call arguments, got %d", len(argsList))
}
// OpenAI path passes Arguments through as strings — verify key order is preserved
// Block 0: keys should be description, timeout, command
s0 := argsList[0]
if !(strings.Index(s0, "description") < strings.Index(s0, "timeout") &&
strings.Index(s0, "timeout") < strings.Index(s0, "command")) {
t.Errorf("block 0: key order not preserved, expected description < timeout < command in: %s", s0)
}
// Block 1: keys should be command, description
s1 := argsList[1]
if !(strings.Index(s1, "command") < strings.Index(s1, "description")) {
t.Errorf("block 1: key order not preserved, expected command < description in: %s", s1)
}
}

View File

@@ -0,0 +1,92 @@
package azure_test
import (
"os"
"strings"
"testing"
"github.com/maximhq/bifrost/core/internal/llmtests"
"github.com/maximhq/bifrost/core/schemas"
)
func TestAzure(t *testing.T) {
t.Parallel()
if strings.TrimSpace(os.Getenv("AZURE_API_KEY")) == "" {
t.Skip("Skipping Azure tests because AZURE_API_KEY is not set")
}
client, ctx, cancel, err := llmtests.SetupTest()
if err != nil {
t.Fatalf("Error initializing test setup: %v", err)
}
defer cancel()
defer client.Shutdown()
testConfig := llmtests.ComprehensiveTestConfig{
Provider: schemas.Azure,
ChatModel: "gpt-4o",
PromptCachingModel: "gpt-4o",
VisionModel: "gpt-4o",
ChatAudioModel: "gpt-4o-mini-audio-preview",
Fallbacks: []schemas.Fallback{
{Provider: schemas.Azure, Model: "gpt-4o"},
},
TextModel: "", // Azure doesn't support text completion in newer models
EmbeddingModel: "text-embedding-ada-002",
ReasoningModel: "claude-opus-4-5",
SpeechSynthesisModel: "gpt-4o-mini-tts",
TranscriptionModel: "whisper",
ImageGenerationModel: "gpt-image-1",
ImageEditModel: "gpt-image-1",
VideoGenerationModel: "sora-2",
PassthroughModel: "gpt-4o",
Scenarios: llmtests.TestScenarios{
TextCompletion: false, // Not supported
SimpleChat: true,
CompletionStream: true,
MultiTurnConversation: true,
ToolCalls: true,
ToolCallsStreaming: true,
MultipleToolCalls: true,
MultipleToolCallsStreaming: true,
End2EndToolCalling: true,
AutomaticFunctionCall: true,
ImageURL: true,
ImageBase64: true,
MultipleImages: true,
CompleteEnd2End: true,
Embedding: true,
ListModels: true,
Reasoning: true,
ChatAudio: false,
Transcription: false, // Disabled for azure because of 3 calls/minute quota
TranscriptionStream: false, // Not properly supported yet by Azure
SpeechSynthesis: false, // Disabled for azure because of 3 calls/minute quota
SpeechSynthesisStream: false, // Disabled for azure because of 3 calls/minute quota
StructuredOutputs: true, // Structured outputs with nullable enum support
PromptCaching: true,
ImageGeneration: false, // Skipped for Azure
ImageGenerationStream: false, // Skipped for Azure
ImageEdit: false, // Model not deployed on Azure endpoint
ImageEditStream: false, // Model not deployed on Azure endpoint
ImageVariation: false, // Not supported by Azure
VideoGeneration: false, // disabled for now because of long running operations
VideoDownload: false,
VideoRetrieve: false,
VideoRemix: false,
VideoList: false,
VideoDelete: false,
InterleavedThinking: true,
PassthroughAPI: true,
EagerInputStreaming: true, // fine-grained-tool-streaming-2025-05-14 (Beta on Azure Foundry)
ServerToolsViaOpenAIEndpoint: true, // web_search / web_fetch / code_execution on Azure per Table 20
},
DisableParallelFor: []string{"Transcription"}, // Azure Whisper has 3 calls/minute quota
}
t.Run("AzureTests", func(t *testing.T) {
llmtests.RunAllComprehensiveTests(t, client, ctx, testConfig)
})
}

View File

@@ -0,0 +1,122 @@
package azure
import (
"context"
"fmt"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/maximhq/bifrost/core/providers/openai"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
"github.com/maximhq/bifrost/core/schemas"
"github.com/valyala/fasthttp"
)
// setAzureAuth sets the Azure authentication header on the request for OpenAI models.
// It handles authentication in order of priority:
// 1. Service Principal (client ID/secret/tenant ID) - uses Bearer token
// 2. Context token - uses Bearer token
// 3. API key - uses api-key header
// 4. DefaultAzureCredential auto-detection (managed identity, workload identity, env vars, CLI)
func (provider *AzureProvider) setAzureAuth(ctx context.Context, req *fasthttp.Request, key schemas.Key) *schemas.BifrostError {
// Service Principal authentication
if key.AzureKeyConfig != nil && key.AzureKeyConfig.ClientID != nil &&
key.AzureKeyConfig.ClientSecret != nil && key.AzureKeyConfig.TenantID != nil && key.AzureKeyConfig.ClientID.GetValue() != "" && key.AzureKeyConfig.ClientSecret.GetValue() != "" && key.AzureKeyConfig.TenantID.GetValue() != "" {
cred, err := provider.getOrCreateAuth(key.AzureKeyConfig.TenantID.GetValue(), key.AzureKeyConfig.ClientID.GetValue(), key.AzureKeyConfig.ClientSecret.GetValue())
if err != nil {
return providerUtils.NewBifrostOperationError("failed to get or create Azure authentication", err)
}
scopes := getAzureScopes(key.AzureKeyConfig.Scopes)
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: scopes,
})
if err != nil {
return providerUtils.NewBifrostOperationError("failed to get Azure access token", err)
}
if token.Token == "" {
return providerUtils.NewBifrostOperationError("azure access token is empty", fmt.Errorf("token is empty"))
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Token))
req.Header.Del("api-key")
return nil
}
// Context token authentication
if authToken, ok := ctx.Value(AzureAuthorizationTokenKey).(string); ok && authToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken))
req.Header.Del("api-key")
return nil
}
// API key authentication
value := key.Value.GetValue()
if value != "" {
req.Header.Del("Authorization")
req.Header.Set("api-key", value)
return nil
}
// No explicit credentials - attempt DefaultAzureCredential auto-detection.
scopes := getAzureScopes(nil)
if key.AzureKeyConfig != nil {
scopes = getAzureScopes(key.AzureKeyConfig.Scopes)
}
cred, err := provider.getOrCreateDefaultAzureCredential()
if err != nil {
return providerUtils.NewBifrostOperationError("no credentials provided and DefaultAzureCredential unavailable", err)
}
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: scopes})
if err != nil {
return providerUtils.NewBifrostOperationError("no credentials provided and DefaultAzureCredential failed to get token", err)
}
if token.Token == "" {
return providerUtils.NewBifrostOperationError("no credentials provided and DefaultAzureCredential returned empty token", fmt.Errorf("token is empty"))
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Token))
req.Header.Del("api-key")
return nil
}
// AzureFileResponse represents an Azure file response (same as OpenAI).
type AzureFileResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Bytes int64 `json:"bytes"`
CreatedAt int64 `json:"created_at"`
Filename string `json:"filename"`
Purpose schemas.FilePurpose `json:"purpose"`
Status string `json:"status,omitempty"`
StatusDetails *string `json:"status_details,omitempty"`
}
// ToBifrostFileUploadResponse converts Azure file response to Bifrost response.
func (r *AzureFileResponse) ToBifrostFileUploadResponse(providerName schemas.ModelProvider, latency time.Duration, sendBackRawResponse bool, rawResponse interface{}) *schemas.BifrostFileUploadResponse {
resp := &schemas.BifrostFileUploadResponse{
ID: r.ID,
Object: r.Object,
Bytes: r.Bytes,
CreatedAt: r.CreatedAt,
Filename: r.Filename,
Purpose: r.Purpose,
Status: openai.ToBifrostFileStatus(r.Status),
StatusDetails: r.StatusDetails,
StorageBackend: schemas.FileStorageAPI,
ExtraFields: schemas.BifrostResponseExtraFields{
Latency: latency.Milliseconds(),
},
}
if sendBackRawResponse {
resp.ExtraFields.RawResponse = rawResponse
}
return resp
}

View File

@@ -0,0 +1,51 @@
package azure
import (
"strings"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
"github.com/maximhq/bifrost/core/schemas"
)
func (response *AzureListModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse {
if response == nil {
return nil
}
bifrostResponse := &schemas.BifrostListModelsResponse{
Data: make([]schemas.Model, 0, len(response.Data)),
}
pipeline := &providerUtils.ListModelsPipeline{
AllowedModels: allowedModels,
BlacklistedModels: blacklistedModels,
Aliases: aliases,
Unfiltered: unfiltered,
ProviderKey: schemas.Azure,
MatchFns: providerUtils.DefaultMatchFns(),
}
if pipeline.ShouldEarlyExit() {
return bifrostResponse
}
included := make(map[string]bool)
for _, model := range response.Data {
for _, result := range pipeline.FilterModel(model.ID) {
entry := schemas.Model{
ID: string(schemas.Azure) + "/" + result.ResolvedID,
Created: schemas.Ptr(model.CreatedAt),
}
if result.AliasValue != "" {
entry.Alias = schemas.Ptr(result.AliasValue)
}
bifrostResponse.Data = append(bifrostResponse.Data, entry)
included[strings.ToLower(result.ResolvedID)] = true
}
}
bifrostResponse.Data = append(bifrostResponse.Data,
pipeline.BackfillModels(included)...)
return bifrostResponse
}

View File

@@ -0,0 +1,36 @@
package azure
// AzureAPIVersionDefault is the default Azure API version to use when not specified.
const AzureAPIVersionDefault = "2024-10-21"
const AzureAPIVersionPreview = "preview"
const AzureAPIVersionImageEditDefault = "2025-04-01-preview"
const AzureAnthropicAPIVersionDefault = "2023-06-01"
type AzureModelCapabilities struct {
FineTune bool `json:"fine_tune"`
Inference bool `json:"inference"`
Completion bool `json:"completion"`
ChatCompletion bool `json:"chat_completion"`
Embeddings bool `json:"embeddings"`
}
type AzureModelDeprecation struct {
FineTune int64 `json:"fine_tune,omitempty"`
Inference int64 `json:"inference,omitempty"`
}
type AzureModel struct {
ID string `json:"id"`
Status string `json:"status"`
FineTune string `json:"fine_tune,omitempty"`
Capabilities AzureModelCapabilities `json:"capabilities,omitempty"`
LifecycleStatus string `json:"lifecycle_status"`
Deprecation *AzureModelDeprecation `json:"deprecation,omitempty"`
CreatedAt int64 `json:"created_at"`
Object string `json:"object"`
}
type AzureListModelsResponse struct {
Object string `json:"object"`
Data []AzureModel `json:"data"`
}

View File

@@ -0,0 +1,94 @@
package azure
import (
"fmt"
"strings"
"github.com/maximhq/bifrost/core/providers/anthropic"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
"github.com/maximhq/bifrost/core/schemas"
)
func getRequestBodyForAnthropicResponses(ctx *schemas.BifrostContext, request *schemas.BifrostResponsesRequest, deployment string, isStreaming bool) ([]byte, *schemas.BifrostError) {
// Large payload mode: body streams directly from the LP reader — skip all body building
// (matches CheckContextAndGetRequestBody guard).
if providerUtils.IsLargePayloadPassthroughEnabled(ctx) {
return nil, nil
}
var jsonBody []byte
var err error
// Check if raw request body should be used
if useRawBody, ok := ctx.Value(schemas.BifrostContextKeyUseRawRequestBody).(bool); ok && useRawBody {
jsonBody = request.GetRawRequestBody()
// Add max_tokens if not present (using sjson to preserve key order for prompt caching)
if !providerUtils.JSONFieldExists(jsonBody, "max_tokens") {
jsonBody, err = providerUtils.SetJSONField(jsonBody, "max_tokens", providerUtils.GetMaxOutputTokensOrDefault(deployment, anthropic.AnthropicDefaultMaxTokens))
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err)
}
}
// Replace model with deployment
jsonBody, err = providerUtils.SetJSONField(jsonBody, "model", deployment)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err)
}
// Delete fallbacks field
jsonBody, err = providerUtils.DeleteJSONField(jsonBody, "fallbacks")
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err)
}
// Add stream if streaming
if isStreaming {
jsonBody, err = providerUtils.SetJSONField(jsonBody, "stream", true)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err)
}
}
} else {
// Convert request to Anthropic format
request.Model = deployment
reqBody, convErr := anthropic.ToAnthropicResponsesRequest(ctx, request)
if convErr != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrRequestBodyConversion, convErr)
}
if reqBody == nil {
return nil, providerUtils.NewBifrostOperationError("request body is not provided", nil)
}
if isStreaming {
reqBody.Stream = schemas.Ptr(true)
}
// Add provider-aware beta headers for Azure
anthropic.AddMissingBetaHeadersToContext(ctx, reqBody, schemas.Azure)
// Marshal struct to JSON bytes, preserving field order
jsonBody, err = providerUtils.MarshalSorted(reqBody)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, fmt.Errorf("failed to marshal request body: %w", err))
}
}
return jsonBody, nil
}
// getCleanedScopes returns cleaned scopes or default scope if none are valid.
// It filters out empty/whitespace-only strings and returns the default scope if no valid scopes remain.
func getAzureScopes(configuredScopes []string) []string {
scopes := []string{DefaultAzureScope}
if len(configuredScopes) > 0 {
cleaned := make([]string, 0, len(configuredScopes))
for _, s := range configuredScopes {
if strings.TrimSpace(s) != "" {
cleaned = append(cleaned, strings.TrimSpace(s))
}
}
if len(cleaned) > 0 {
scopes = cleaned
}
}
return scopes
}