first commit
This commit is contained in:
2958
core/providers/azure/azure.go
Normal file
2958
core/providers/azure/azure.go
Normal file
File diff suppressed because it is too large
Load Diff
198
core/providers/azure/azure_caching_test.go
Normal file
198
core/providers/azure/azure_caching_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
92
core/providers/azure/azure_test.go
Normal file
92
core/providers/azure/azure_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
122
core/providers/azure/files.go
Normal file
122
core/providers/azure/files.go
Normal 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
|
||||
}
|
||||
51
core/providers/azure/models.go
Normal file
51
core/providers/azure/models.go
Normal 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
|
||||
}
|
||||
36
core/providers/azure/types.go
Normal file
36
core/providers/azure/types.go
Normal 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"`
|
||||
}
|
||||
94
core/providers/azure/utils.go
Normal file
94
core/providers/azure/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user