Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -6167,7 +6167,13 @@ func (bifrost *Bifrost) requestWorker(provider schemas.Provider, config *schemas
// returned to the pool via its deferred finalizer.
if IsStreamRequestType(req.RequestType) {
stream, bifrostError = executeRequestWithRetries(req.Context, config, func(k schemas.Key) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
resolvedModel = k.Aliases.Resolve(originalModelRequested)
if aliasConfig := k.Aliases.ResolveConfig(originalModelRequested); aliasConfig != nil {
resolvedModel = aliasConfig.ModelID
req.Context.SetValue(schemas.BifrostContextKeyResolvedAlias, &schemas.ResolvedAlias{Key: originalModelRequested, Config: aliasConfig})
} else {
resolvedModel = originalModelRequested
req.Context.SetValue(schemas.BifrostContextKeyResolvedAlias, nil)
}
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
req.SetModel(resolvedModel)
// Snapshot per-attempt so postHookRunner doesn't observe a later retry's
// alias while this attempt's provider goroutine is still emitting chunks.
Expand Down Expand Up @@ -6226,7 +6232,13 @@ func (bifrost *Bifrost) requestWorker(provider schemas.Provider, config *schemas
}, keyProvider, req.RequestType, provider.GetProviderKey(), model, &req.BifrostRequest, bifrost.logger)
} else {
result, bifrostError = executeRequestWithRetries(req.Context, config, func(k schemas.Key) (*schemas.BifrostResponse, *schemas.BifrostError) {
resolvedModel = k.Aliases.Resolve(originalModelRequested)
if aliasConfig := k.Aliases.ResolveConfig(originalModelRequested); aliasConfig != nil {
resolvedModel = aliasConfig.ModelID
req.Context.SetValue(schemas.BifrostContextKeyResolvedAlias, &schemas.ResolvedAlias{Key: originalModelRequested, Config: aliasConfig})
} else {
resolvedModel = originalModelRequested
req.Context.SetValue(schemas.BifrostContextKeyResolvedAlias, nil)
}
req.SetModel(resolvedModel)
return bifrost.handleProviderRequest(provider, config, req, k, keys)
}, keyProvider, req.RequestType, provider.GetProviderKey(), model, &req.BifrostRequest, bifrost.logger)
Expand Down
224 changes: 152 additions & 72 deletions core/providers/azure/azure.go

Large diffs are not rendered by default.

145 changes: 144 additions & 1 deletion core/providers/azure/azure_passthrough_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,153 @@ func TestBuildPassthroughURL(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := provider.buildPassthroughURL(makeKey(endpoint), tt.path, tt.rawQuery)
got, _ := provider.buildPassthroughURL(nil, makeKey(endpoint), tt.path, tt.rawQuery)
if got != tt.want {
t.Errorf("\ngot: %s\nwant: %s", got, tt.want)
}
})
}
}

// TestBuildPassthroughURL_AliasAPIVersionOverride verifies that when the
// resolved alias carries an AzureAliasCfg.APIVersion override, it takes
// precedence over the route default (DefaultAzureAPIVersion for /deployments/,
// AzureAPIVersionPreview for /openai/v1/responses) — only in the path where
// the caller did NOT supply api-version themselves. Caller-supplied wins over
// alias override; alias override wins over route default.
func TestBuildPassthroughURL_AliasAPIVersionOverride(t *testing.T) {
t.Parallel()

provider := &AzureProvider{}
endpoint := "https://my-resource.openai.azure.com"
makeKey := schemas.Key{
AzureKeyConfig: &schemas.AzureKeyConfig{
Endpoint: *schemas.NewEnvVar(endpoint),
},
}

// Build a ctx carrying an alias with APIVersion override.
overrideVer := "2024-10-21"
ctx := schemas.NewBifrostContext(nil, schemas.NoDeadline)
ctx.SetValue(schemas.BifrostContextKeyResolvedAlias, &schemas.ResolvedAlias{
Key: "best-model",
Config: &schemas.AliasConfig{
ModelID: "gpt-4o-deployment",
AzureAliasCfg: &schemas.AzureAliasCfg{
APIVersion: &overrideVer,
},
},
})

t.Run("deployments route: alias APIVersion overrides default", func(t *testing.T) {
got, _ := provider.buildPassthroughURL(ctx, makeKey, "/openai/deployments/gpt-4o/chat/completions", "")
want := endpoint + "/openai/deployments/gpt-4o/chat/completions?api-version=" + overrideVer
if got != want {
t.Errorf("\ngot: %s\nwant: %s", got, want)
}
})

t.Run("responses route: alias APIVersion overrides preview default", func(t *testing.T) {
got, _ := provider.buildPassthroughURL(ctx, makeKey, "/openai/v1/responses", "")
want := endpoint + "/openai/v1/responses?api-version=" + overrideVer
if got != want {
t.Errorf("\ngot: %s\nwant: %s", got, want)
}
})

t.Run("caller-supplied api-version wins over alias override", func(t *testing.T) {
got, _ := provider.buildPassthroughURL(ctx, makeKey, "/openai/deployments/gpt-4o/chat/completions", "api-version=2023-01-01")
want := endpoint + "/openai/deployments/gpt-4o/chat/completions?api-version=2023-01-01"
if got != want {
t.Errorf("\ngot: %s\nwant: %s", got, want)
}
})
}

// TestResolveAPIVersion_NoAlias verifies the helper returns the route default
// when no resolved alias is in ctx (covers the legacy code path).
func TestResolveAPIVersion_NoAlias(t *testing.T) {
if got := resolveAPIVersion(nil, DefaultAzureAPIVersion); got != DefaultAzureAPIVersion {
t.Errorf("got %q, want %q", got, DefaultAzureAPIVersion)
}
ctx := schemas.NewBifrostContext(nil, schemas.NoDeadline)
if got := resolveAPIVersion(ctx, AzureAPIVersionPreview); got != AzureAPIVersionPreview {
t.Errorf("got %q, want %q", got, AzureAPIVersionPreview)
}
}

// TestResolveAzureEndpoint_AliasOverride verifies the Endpoint override path.
// Lets one Azure credential cover deployments hosted on multiple cognitive-
// services resources.
func TestResolveAzureEndpoint_AliasOverride(t *testing.T) {
keyEndpoint := "https://primary.openai.azure.com"
aliasEndpoint := "https://anthropic-resource.openai.azure.com"
key := schemas.Key{
AzureKeyConfig: &schemas.AzureKeyConfig{
Endpoint: *schemas.NewEnvVar(keyEndpoint),
},
}

// No alias: falls back to key-level endpoint.
if got := resolveAzureEndpoint(nil, key); got != keyEndpoint {
t.Errorf("nil ctx: got %q, want key-level %q", got, keyEndpoint)
}
ctx := schemas.NewBifrostContext(nil, schemas.NoDeadline)
if got := resolveAzureEndpoint(ctx, key); got != keyEndpoint {
t.Errorf("empty ctx: got %q, want key-level %q", got, keyEndpoint)
}

// With alias-level Endpoint override, alias wins.
override := schemas.NewEnvVar(aliasEndpoint)
ctx.SetValue(schemas.BifrostContextKeyResolvedAlias, &schemas.ResolvedAlias{
Key: "best-claude",
Config: &schemas.AliasConfig{
ModelID: "claude-deployment",
AzureAliasCfg: &schemas.AzureAliasCfg{
Endpoint: override,
},
},
})
if got := resolveAzureEndpoint(ctx, key); got != aliasEndpoint {
t.Errorf("alias override: got %q, want %q", got, aliasEndpoint)
}

// Alias with empty Endpoint value falls through to key-level — guards against
// a misconfigured alias accidentally erasing the endpoint.
emptyOverride := schemas.NewEnvVar("")
ctx2 := schemas.NewBifrostContext(nil, schemas.NoDeadline)
ctx2.SetValue(schemas.BifrostContextKeyResolvedAlias, &schemas.ResolvedAlias{
Key: "x",
Config: &schemas.AliasConfig{
ModelID: "x",
AzureAliasCfg: &schemas.AzureAliasCfg{
Endpoint: emptyOverride,
},
},
})
if got := resolveAzureEndpoint(ctx2, key); got != keyEndpoint {
t.Errorf("empty alias endpoint should fall through: got %q, want %q", got, keyEndpoint)
}
}

// TestResolveAnthropicVersion_AliasOverride verifies the AnthropicVersion
// override path mirrors the APIVersion behavior.
func TestResolveAnthropicVersion_AliasOverride(t *testing.T) {
if got := resolveAnthropicVersion(nil); got != AzureAnthropicAPIVersionDefault {
t.Errorf("nil ctx: got %q, want default", got)
}
override := "2024-10-22"
ctx := schemas.NewBifrostContext(nil, schemas.NoDeadline)
ctx.SetValue(schemas.BifrostContextKeyResolvedAlias, &schemas.ResolvedAlias{
Key: "best-claude",
Config: &schemas.AliasConfig{
ModelID: "claude-deployment",
AzureAliasCfg: &schemas.AzureAliasCfg{
AnthropicVersion: &override,
},
},
})
if got := resolveAnthropicVersion(ctx); got != override {
t.Errorf("got %q, want %q", got, override)
}
}
42 changes: 42 additions & 0 deletions core/providers/azure/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,45 @@ func getAzureScopes(configuredScopes []string) []string {
}
return scopes
}

// resolveAnthropicVersion returns the anthropic-version header value for the
// current attempt. Uses the AzureAliasCfg.AnthropicVersion override from the
// resolved alias when present, otherwise the Azure default.
func resolveAnthropicVersion(ctx *schemas.BifrostContext) string {
if ra := schemas.GetResolvedAlias(ctx); ra != nil && ra.Config != nil && ra.Config.AzureAliasCfg != nil && ra.Config.AzureAliasCfg.AnthropicVersion != nil && *ra.Config.AzureAliasCfg.AnthropicVersion != "" {
return *ra.Config.AzureAliasCfg.AnthropicVersion
}
return AzureAnthropicAPIVersionDefault
}

// resolveAPIVersion returns the Azure api-version query parameter value for
// the current attempt. Uses the AzureAliasCfg.APIVersion override from the
// resolved alias when present, otherwise the provided default. Different
// Azure routes have different defaults (DefaultAzureAPIVersion for classic
// /openai/deployments/, AzureAPIVersionPreview for /openai/v1/responses);
// callers pass the route's default so the override can take precedence
// without losing the route-specific fallback.
func resolveAPIVersion(ctx *schemas.BifrostContext, defaultVersion string) string {
if ra := schemas.GetResolvedAlias(ctx); ra != nil && ra.Config != nil && ra.Config.AzureAliasCfg != nil && ra.Config.AzureAliasCfg.APIVersion != nil && *ra.Config.AzureAliasCfg.APIVersion != "" {
return *ra.Config.AzureAliasCfg.APIVersion
}
return defaultVersion
}

// resolveAzureEndpoint returns the Azure cognitive-services endpoint URL for
// the current attempt. Uses the AzureAliasCfg.Endpoint override from the
// resolved alias when present, otherwise the key-level endpoint. Lets one
// Azure credential (ClientID/Secret/TenantID or API key) span deployments
// hosted on different Azure resources (e.g. OpenAI on east-us, Anthropic on
// west-us2).
func resolveAzureEndpoint(ctx *schemas.BifrostContext, key schemas.Key) string {
if ra := schemas.GetResolvedAlias(ctx); ra != nil && ra.Config != nil && ra.Config.AzureAliasCfg != nil && ra.Config.AzureAliasCfg.Endpoint != nil {
if v := ra.Config.AzureAliasCfg.Endpoint.GetValue(); v != "" {
return v
}
}
if key.AzureKeyConfig != nil {
return key.AzureKeyConfig.Endpoint.GetValue()
}
return ""
}
87 changes: 83 additions & 4 deletions core/schemas/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"slices"
"strings"

"github.com/bytedance/sonic"
)

type KeyStatusType string
Expand Down Expand Up @@ -175,7 +177,7 @@ func (mf *ModelFamily) IsValid() bool {
type AzureAliasCfg struct {
APIVersion *string `json:"api_version,omitempty"` // overrides the Azure OpenAI api-version query param for this alias
AnthropicVersion *string `json:"anthropic_version,omitempty"` // overrides the anthropic-version header for Claude-on-Azure deployments
Endpoint *EnvVar `json:"endpoint,omitempty"` // overrides AzureKeyConfig.Endpoint for this alias (allows one credential to span multiple Azure resources)
Endpoint *EnvVar `json:"endpoint,omitempty"` // overrides AzureKeyConfig.Endpoint for this alias — lets one credential span deployments on multiple Azure resources
}

// VertexAliasCfg holds Vertex-specific overrides that apply to a single alias.
Expand Down Expand Up @@ -301,6 +303,83 @@ func (ka KeyAliases) Resolve(model string) string {
return model
}

// ResolvedAlias is what core stashes in BifrostContext after key-level alias
// resolution. Key is the user-facing model name the client sent (LHS of the
// alias map). Config is the matched AliasConfig.
//
// Carrying the alias key alongside the config lets providers consult it as
// the lowest-precedence tier for family detection — common case: an admin
// names their alias "best-claude" but the wire ModelID is an opaque Azure
// deployment ID, so neither the config fields nor request.Model carry the
// "claude" substring; the alias key does.
type ResolvedAlias struct {
Key string
Config *AliasConfig
}

// GetResolvedAlias returns the ResolvedAlias that core stashed in ctx after
// key-level alias resolution, or nil if no alias matched or ctx is nil.
//
// This is set by bifrost.go alongside req.SetModel(resolved). Plugins must
// not write to this key directly.
func GetResolvedAlias(ctx *BifrostContext) *ResolvedAlias {
if ctx == nil {
return nil
}
v := ctx.Value(BifrostContextKeyResolvedAlias)
if v == nil {
return nil
}
ra, _ := v.(*ResolvedAlias)
return ra
}

// ResolveFamily returns the model family for the current attempt, walking
// the precedence: explicit alias ModelFamily → alias ModelName → alias
// ModelID → alias Key. When no alias matched, falls back to substring
// matching against fallbackModel (typically request.Model), preserving
// pre-refactor behavior.
//
// Returns an empty ModelFamily if nothing matches.
func ResolveFamily(ctx *BifrostContext, fallbackModel string) ModelFamily {
ra := GetResolvedAlias(ctx)
var candidates []string
if ra != nil && ra.Config != nil {
if ra.Config.ModelFamily != nil && *ra.Config.ModelFamily != "" {
return *ra.Config.ModelFamily
}
if ra.Config.ModelName != nil {
candidates = append(candidates, *ra.Config.ModelName)
}
candidates = append(candidates, ra.Config.ModelID, ra.Key)
} else {
candidates = append(candidates, fallbackModel)
}
for _, s := range candidates {
switch {
case IsAnthropicModel(s):
return ModelFamilyAnthropic
case IsMistralModel(s):
return ModelFamilyMistral
case IsGeminiModel(s):
return ModelFamilyGemini
case IsNovaModel(s):
return ModelFamilyNova
}
}
return ""
}

// IsAnthropicModelFamily reports whether the current attempt resolves to the
// Anthropic model family. Thin wrapper over ResolveFamily so provider code
// reads uniformly at the many call sites that branch on Anthropic vs
// non-Anthropic (request shape, response parsing, anthropic-version header,
// URL path construction). model is passed as the substring-match fallback
// used when no alias is resolved in ctx — typically request.Model.
func IsAnthropicModelFamily(ctx *BifrostContext, model string) bool {
return ResolveFamily(ctx, model) == ModelFamilyAnthropic
}

// ResolveConfig returns the AliasConfig for the given user-facing model name,
// or nil if no alias matches. Case-insensitive fallback matches Resolve.
func (ka KeyAliases) ResolveConfig(model string) *AliasConfig {
Expand Down Expand Up @@ -328,7 +407,7 @@ func (ka *KeyAliases) UnmarshalJSON(data []byte) error {
return nil
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
if err := sonic.Unmarshal(data, &raw); err != nil {
return err
}
result := make(KeyAliases, len(raw))
Expand All @@ -341,13 +420,13 @@ func (ka *KeyAliases) UnmarshalJSON(data []byte) error {
case '"':
// Legacy string value — promote to AliasConfig{ModelID: ...}.
var modelID string
if err := json.Unmarshal(entry, &modelID); err != nil {
if err := sonic.Unmarshal(entry, &modelID); err != nil {
return fmt.Errorf("alias %q: %w", k, err)
}
result[k] = AliasConfig{ModelID: modelID}
case '{':
var ac AliasConfig
if err := json.Unmarshal(entry, &ac); err != nil {
if err := sonic.Unmarshal(entry, &ac); err != nil {
return fmt.Errorf("alias %q: %w", k, err)
}
result[k] = ac
Expand Down
Loading
Loading