From d58db11023c29622dce2a5df01785758306c05ac Mon Sep 17 00:00:00 2001 From: Pratham-Mishra04 Date: Tue, 2 Jun 2026 13:31:25 +0530 Subject: [PATCH] feat: extend key aliases to support deployment level configurations --- core/internal/llmtests/account.go | 54 +-- core/providers/anthropic/models.go | 2 +- core/providers/azure/models.go | 2 +- core/providers/bedrock/models.go | 2 +- core/providers/bedrock/rerank_test.go | 4 +- core/providers/cohere/models.go | 2 +- core/providers/elevenlabs/models.go | 2 +- core/providers/gemini/models.go | 2 +- core/providers/huggingface/models.go | 2 +- core/providers/mistral/models.go | 2 +- core/providers/openai/models.go | 2 +- core/providers/openrouter/openrouter.go | 6 +- core/providers/replicate/models.go | 2 +- core/providers/utils/models.go | 20 +- core/providers/vertex/models.go | 4 +- core/providers/vertex/utils.go | 4 +- core/schemas/account.go | 196 ++++++++++- core/schemas/account_test.go | 308 ++++++++++++++++++ framework/configstore/encryption_test.go | 2 +- framework/configstore/keyhash_alias_test.go | 76 +++++ framework/configstore/migrations_test.go | 4 +- .../configstore/tables/encryption_test.go | 144 +++++++- transports/bifrost-http/lib/config_test.go | 130 ++++---- transports/config.schema.json | 67 +++- 24 files changed, 897 insertions(+), 142 deletions(-) create mode 100644 core/schemas/account_test.go create mode 100644 framework/configstore/keyhash_alias_test.go diff --git a/core/internal/llmtests/account.go b/core/internal/llmtests/account.go index c78ffbd508f..6a2b0e5d8d1 100644 --- a/core/internal/llmtests/account.go +++ b/core/internal/llmtests/account.go @@ -241,12 +241,12 @@ func (account *ComprehensiveTestAccount) GetKeysForProvider(ctx context.Context, { Models: []string{"*"}, Weight: 1.0, - Aliases: map[string]string{ - "claude-3.7-sonnet": "us.anthropic.claude-3-7-sonnet-20250219-v1:0", - "claude-4-sonnet": "global.anthropic.claude-sonnet-4-20250514-v1:0", - "claude-4.5-sonnet": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", - "claude-4.6-sonnet": "global.anthropic.claude-sonnet-4-6", - "claude-4.5-haiku": "global.anthropic.claude-haiku-4-5-20251001-v1:0", + Aliases: schemas.KeyAliases{ + "claude-3.7-sonnet": {ModelID: "us.anthropic.claude-3-7-sonnet-20250219-v1:0"}, + "claude-4-sonnet": {ModelID: "global.anthropic.claude-sonnet-4-20250514-v1:0"}, + "claude-4.5-sonnet": {ModelID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0"}, + "claude-4.6-sonnet": {ModelID: "global.anthropic.claude-sonnet-4-6"}, + "claude-4.5-haiku": {ModelID: "global.anthropic.claude-haiku-4-5-20251001-v1:0"}, }, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("env.AWS_ACCESS_KEY_ID"), @@ -259,13 +259,13 @@ func (account *ComprehensiveTestAccount) GetKeysForProvider(ctx context.Context, { Models: []string{"*"}, Weight: 1.0, - Aliases: map[string]string{ - "claude-3.5-sonnet": "anthropic.claude-3-5-sonnet-20240620-v1:0", - "claude-3.7-sonnet": "us.anthropic.claude-3-7-sonnet-20250219-v1:0", - "claude-4-sonnet": "global.anthropic.claude-sonnet-4-20250514-v1:0", - "claude-4.5-sonnet": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", - "claude-4.6-sonnet": "global.anthropic.claude-sonnet-4-6", - "claude-4.5-haiku": "global.anthropic.claude-haiku-4-5-20251001-v1:0", + Aliases: schemas.KeyAliases{ + "claude-3.5-sonnet": {ModelID: "anthropic.claude-3-5-sonnet-20240620-v1:0"}, + "claude-3.7-sonnet": {ModelID: "us.anthropic.claude-3-7-sonnet-20250219-v1:0"}, + "claude-4-sonnet": {ModelID: "global.anthropic.claude-sonnet-4-20250514-v1:0"}, + "claude-4.5-sonnet": {ModelID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0"}, + "claude-4.6-sonnet": {ModelID: "global.anthropic.claude-sonnet-4-6"}, + "claude-4.5-haiku": {ModelID: "global.anthropic.claude-haiku-4-5-20251001-v1:0"}, }, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("env.AWS_ACCESS_KEY_ID"), @@ -303,13 +303,13 @@ func (account *ComprehensiveTestAccount) GetKeysForProvider(ctx context.Context, Models: []string{"*"}, Weight: 1.0, Aliases: schemas.KeyAliases{ - "gpt-4o": "gpt-4o", - "gpt-4o-backup": "gpt-4o-3", - "claude-opus-4-5": "claude-opus-4-5", - "o1": "o1", - "gpt-image-1": "gpt-image-1", - "text-embedding-ada-002": "text-embedding-ada-002", - "sora-2": "sora-2", + "gpt-4o": {ModelID: "gpt-4o"}, + "gpt-4o-backup": {ModelID: "gpt-4o-3"}, + "claude-opus-4-5": {ModelID: "claude-opus-4-5"}, + "o1": {ModelID: "o1"}, + "gpt-image-1": {ModelID: "gpt-image-1"}, + "text-embedding-ada-002": {ModelID: "text-embedding-ada-002"}, + "sora-2": {ModelID: "sora-2"}, }, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("env.AZURE_ENDPOINT"), @@ -324,10 +324,10 @@ func (account *ComprehensiveTestAccount) GetKeysForProvider(ctx context.Context, Models: []string{"*"}, Weight: 1.0, Aliases: schemas.KeyAliases{ - "whisper": "whisper", - "whisper-1": "whisper", - "gpt-4o-mini-tts": "gpt-4o-mini-tts", - "gpt-4o-mini-audio-preview": "gpt-4o-mini-audio-preview", + "whisper": {ModelID: "whisper"}, + "whisper-1": {ModelID: "whisper"}, + "gpt-4o-mini-tts": {ModelID: "gpt-4o-mini-tts"}, + "gpt-4o-mini-audio-preview": {ModelID: "gpt-4o-mini-audio-preview"}, }, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("env.AZURE_ENDPOINT"), @@ -365,9 +365,9 @@ func (account *ComprehensiveTestAccount) GetKeysForProvider(ctx context.Context, Models: []string{"claude-sonnet-4-5", "claude-4.5-haiku", "claude-opus-4-5"}, Weight: 1.0, Aliases: schemas.KeyAliases{ - "claude-sonnet-4-5": "claude-sonnet-4-5", - "claude-4.5-haiku": "claude-haiku-4-5@20251001", - "claude-opus-4-5": "claude-opus-4-5", + "claude-sonnet-4-5": {ModelID: "claude-sonnet-4-5"}, + "claude-4.5-haiku": {ModelID: "claude-haiku-4-5@20251001"}, + "claude-opus-4-5": {ModelID: "claude-opus-4-5"}, }, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("env.VERTEX_PROJECT_ID"), diff --git a/core/providers/anthropic/models.go b/core/providers/anthropic/models.go index 3815a0244be..a09174c661a 100644 --- a/core/providers/anthropic/models.go +++ b/core/providers/anthropic/models.go @@ -8,7 +8,7 @@ import ( "github.com/maximhq/bifrost/core/schemas" ) -func (response *AnthropicListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *AnthropicListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/azure/models.go b/core/providers/azure/models.go index 5daca3836df..99f4c5ae86d 100644 --- a/core/providers/azure/models.go +++ b/core/providers/azure/models.go @@ -7,7 +7,7 @@ import ( "github.com/maximhq/bifrost/core/schemas" ) -func (response *AzureListModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *AzureListModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/bedrock/models.go b/core/providers/bedrock/models.go index 549db2e3bd8..817a43f2b41 100644 --- a/core/providers/bedrock/models.go +++ b/core/providers/bedrock/models.go @@ -81,7 +81,7 @@ type BedrockRerankResponseDocument struct { TextDocument *BedrockRerankTextValue `json:"textDocument,omitempty"` } -func (response *BedrockListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *BedrockListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/bedrock/rerank_test.go b/core/providers/bedrock/rerank_test.go index c1b7bb54806..5d55dccc697 100644 --- a/core/providers/bedrock/rerank_test.go +++ b/core/providers/bedrock/rerank_test.go @@ -196,7 +196,7 @@ func TestBedrockRerankRequestToBifrostRerankRequestNil(t *testing.T) { func TestResolveBedrockDeployment(t *testing.T) { key := schemas.Key{ Aliases: schemas.KeyAliases{ - "cohere-rerank": "arn:aws:bedrock:us-east-1::foundation-model/cohere.rerank-v3-5:0", + "cohere-rerank": {ModelID: "arn:aws:bedrock:us-east-1::foundation-model/cohere.rerank-v3-5:0"}, }, } @@ -211,7 +211,7 @@ func TestBedrockRerankRequiresARNModelIdentifier(t *testing.T) { ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) key := schemas.Key{ Aliases: schemas.KeyAliases{ - "cohere-rerank": "cohere.rerank-v3-5:0", + "cohere-rerank": {ModelID: "cohere.rerank-v3-5:0"}, }, } diff --git a/core/providers/cohere/models.go b/core/providers/cohere/models.go index 3b285f97b69..33120328884 100644 --- a/core/providers/cohere/models.go +++ b/core/providers/cohere/models.go @@ -45,7 +45,7 @@ type CohereRerankMeta struct { Tokens *CohereTokenUsage `json:"tokens,omitempty"` } -func (response *CohereListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *CohereListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/elevenlabs/models.go b/core/providers/elevenlabs/models.go index f762d97ee89..7e3c1f8d34e 100644 --- a/core/providers/elevenlabs/models.go +++ b/core/providers/elevenlabs/models.go @@ -7,7 +7,7 @@ import ( "github.com/maximhq/bifrost/core/schemas" ) -func (response *ElevenlabsListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *ElevenlabsListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/gemini/models.go b/core/providers/gemini/models.go index 7b9f6410eb6..e88387dc66e 100644 --- a/core/providers/gemini/models.go +++ b/core/providers/gemini/models.go @@ -17,7 +17,7 @@ func toGeminiModelResourceName(modelID string) string { return "models/" + modelID } -func (response *GeminiListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *GeminiListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/huggingface/models.go b/core/providers/huggingface/models.go index de615ccec23..3d04ce4936e 100644 --- a/core/providers/huggingface/models.go +++ b/core/providers/huggingface/models.go @@ -14,7 +14,7 @@ const ( maxModelFetchLimit = 1000 ) -func (response *HuggingFaceListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, inferenceProvider inferenceProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *HuggingFaceListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, inferenceProvider inferenceProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/mistral/models.go b/core/providers/mistral/models.go index 8d5fd7f3d6d..7db5154aa60 100644 --- a/core/providers/mistral/models.go +++ b/core/providers/mistral/models.go @@ -7,7 +7,7 @@ import ( "github.com/maximhq/bifrost/core/schemas" ) -func (response *MistralListModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *MistralListModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/openai/models.go b/core/providers/openai/models.go index a76d350d282..ed88255cb39 100644 --- a/core/providers/openai/models.go +++ b/core/providers/openai/models.go @@ -8,7 +8,7 @@ import ( ) // ToBifrostListModelsResponse converts an OpenAI list models response to a Bifrost list models response -func (response *OpenAIListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *OpenAIListModelsResponse) ToBifrostListModelsResponse(providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/openrouter/openrouter.go b/core/providers/openrouter/openrouter.go index eae6de2bf99..516f52477e4 100644 --- a/core/providers/openrouter/openrouter.go +++ b/core/providers/openrouter/openrouter.go @@ -206,9 +206,11 @@ func (provider *OpenRouterProvider) listModelsByKey(ctx *schemas.BifrostContext, for _, m := range key.BlacklistedModels { normalizedBlacklist = append(normalizedBlacklist, stripPrefix(m)) } - normalizedAliases := make(map[string]string, len(key.Aliases)) + normalizedAliases := make(schemas.KeyAliases, len(key.Aliases)) for k, v := range key.Aliases { - normalizedAliases[stripPrefix(k)] = stripPrefix(v) + cfg := v + cfg.ModelID = stripPrefix(v.ModelID) + normalizedAliases[stripPrefix(k)] = cfg } pipeline := &providerUtils.ListModelsPipeline{ diff --git a/core/providers/replicate/models.go b/core/providers/replicate/models.go index 6c0c14dbf75..3d2c4b60816 100644 --- a/core/providers/replicate/models.go +++ b/core/providers/replicate/models.go @@ -14,7 +14,7 @@ func ToBifrostListModelsResponse( providerKey schemas.ModelProvider, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, - aliases map[string]string, + aliases schemas.KeyAliases, unfiltered bool, ) *schemas.BifrostListModelsResponse { bifrostResponse := &schemas.BifrostListModelsResponse{ diff --git a/core/providers/utils/models.go b/core/providers/utils/models.go index dbbbd8071a8..22555f11aec 100644 --- a/core/providers/utils/models.go +++ b/core/providers/utils/models.go @@ -168,9 +168,9 @@ type FilterResult struct { type ListModelsPipeline struct { AllowedModels schemas.WhiteList BlacklistedModels schemas.BlackList - // Aliases maps user-facing alias keys to provider-specific model IDs. - // e.g. {"my-gpt4": "gpt-4-turbo-2024-04-09"} - Aliases map[string]string + // Aliases maps user-facing alias keys to their AliasConfig. The pipeline + // reads AliasConfig.ModelID for matching and Alias surfacing. + Aliases schemas.KeyAliases Unfiltered bool ProviderKey schemas.ModelProvider // MatchFns is the ordered list of equivalence functions used for every @@ -224,9 +224,9 @@ type aliasMatch struct { // → [{key:"gpt-3.5-turbo", value:""}] func (p *ListModelsPipeline) resolveModelID(modelID string) []aliasMatch { var candidates []aliasMatch - for aliasKey, providerID := range p.Aliases { - if matches(modelID, providerID, p.MatchFns) { - candidates = append(candidates, aliasMatch{key: aliasKey, value: providerID}) + for aliasKey, alias := range p.Aliases { + if matches(modelID, alias.ModelID, p.MatchFns) { + candidates = append(candidates, aliasMatch{key: aliasKey, value: alias.ModelID}) } } if len(candidates) == 0 { @@ -369,9 +369,9 @@ func (p *ListModelsPipeline) BackfillModels(included map[string]bool) []schemas. Name: schemas.Ptr(ToDisplayName(entry)), } // If this allowlist entry has an alias, surface the provider-specific ID. - for aliasKey, providerID := range p.Aliases { + for aliasKey, alias := range p.Aliases { if matches(entry, aliasKey, p.MatchFns) { - m.Alias = schemas.Ptr(providerID) + m.Alias = schemas.Ptr(alias.ModelID) break } } @@ -382,7 +382,7 @@ func (p *ListModelsPipeline) BackfillModels(included map[string]bool) []schemas. // Case B: wildcard allowlist — backfill only explicitly configured aliases. if !p.Unfiltered && len(p.Aliases) > 0 { - for aliasKey, providerID := range p.Aliases { + for aliasKey, alias := range p.Aliases { if included[strings.ToLower(aliasKey)] { continue } @@ -400,7 +400,7 @@ func (p *ListModelsPipeline) BackfillModels(included map[string]bool) []schemas. result = append(result, schemas.Model{ ID: string(p.ProviderKey) + "/" + aliasKey, Name: schemas.Ptr(ToDisplayName(aliasKey)), - Alias: schemas.Ptr(providerID), + Alias: schemas.Ptr(alias.ModelID), }) } } diff --git a/core/providers/vertex/models.go b/core/providers/vertex/models.go index d373f587354..e27db45bd9a 100644 --- a/core/providers/vertex/models.go +++ b/core/providers/vertex/models.go @@ -70,7 +70,7 @@ type vertexRerankOptions struct { // - If allowedModels is empty, all models are allowed // - If allowedModels is non-empty, only models/deployments with keys in allowedModels are included // - Deployments map is used to match model IDs to aliases and filter accordingly -func (response *VertexListModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *VertexListModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } @@ -140,7 +140,7 @@ func (response *VertexListModelsResponse) ToBifrostListModelsResponse(allowedMod // ToBifrostListModelsResponse converts a Vertex AI publisher models response to Bifrost's format. // This is for foundation models from the Model Garden (publishers.models.list endpoint). -func (response *VertexListPublisherModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases map[string]string, unfiltered bool) *schemas.BifrostListModelsResponse { +func (response *VertexListPublisherModelsResponse) ToBifrostListModelsResponse(allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList, aliases schemas.KeyAliases, unfiltered bool) *schemas.BifrostListModelsResponse { if response == nil { return nil } diff --git a/core/providers/vertex/utils.go b/core/providers/vertex/utils.go index 7eb9161f6d0..ef806ab7506 100644 --- a/core/providers/vertex/utils.go +++ b/core/providers/vertex/utils.go @@ -242,7 +242,7 @@ func vertexServiceTierHeaderValue(region string, model string, tier schemas.Bifr // buildResponseFromConfig builds a list models response from configured deployments and allowedModels. // This is used when the user has explicitly configured which models they want to use. -func buildResponseFromConfig(deployments map[string]string, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList) *schemas.BifrostListModelsResponse { +func buildResponseFromConfig(deployments schemas.KeyAliases, allowedModels schemas.WhiteList, blacklistedModels schemas.BlackList) *schemas.BifrostListModelsResponse { response := &schemas.BifrostListModelsResponse{ Data: make([]schemas.Model, 0), } @@ -272,7 +272,7 @@ func buildResponseFromConfig(deployments map[string]string, allowedModels schema modelEntry := schemas.Model{ ID: modelID, Name: schemas.Ptr(modelName), - Alias: schemas.Ptr(deploymentValue), + Alias: schemas.Ptr(deploymentValue.ModelID), } response.Data = append(response.Data, modelEntry) diff --git a/core/schemas/account.go b/core/schemas/account.go index 449399f554b..38d80837287 100644 --- a/core/schemas/account.go +++ b/core/schemas/account.go @@ -2,7 +2,9 @@ package schemas import ( + "bytes" "context" + "encoding/json" "fmt" "slices" "strings" @@ -141,22 +143,136 @@ type Key struct { Description string `json:"description,omitempty"` // Description of key } -type KeyAliases map[string]string +// ModelFamily is a typed enum identifying the underlying model family of an alias target. +// It enables provider routing decisions (request shape, response parsing, auth headers, +// URL construction) without substring-sniffing the wire model ID. +type ModelFamily string + +const ( + ModelFamilyAnthropic ModelFamily = "anthropic" + ModelFamilyOpenAI ModelFamily = "openai" + ModelFamilyMistral ModelFamily = "mistral" + ModelFamilyCohere ModelFamily = "cohere" + ModelFamilyGemini ModelFamily = "gemini" + ModelFamilyNova ModelFamily = "nova" + ModelFamilyTitan ModelFamily = "titan" +) + +// IsValid reports whether mf is a recognized model family. +func (mf *ModelFamily) IsValid() bool { + if mf == nil { + return false + } + switch *mf { + case ModelFamilyAnthropic, ModelFamilyOpenAI, ModelFamilyMistral, + ModelFamilyCohere, ModelFamilyGemini, ModelFamilyNova, ModelFamilyTitan: + return true + } + return false +} + +// AzureAliasCfg holds Azure-specific overrides that apply to a single alias. +// Each field, when non-nil, overrides the corresponding key-level default. +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) +} + +// VertexAliasCfg holds Vertex-specific overrides that apply to a single alias. +type VertexAliasCfg struct { + ProjectID *EnvVar `json:"project_id,omitempty"` + ProjectNumber *EnvVar `json:"project_number,omitempty"` +} + +// BedrockAliasCfg holds Bedrock-specific overrides that apply to a single alias. +type BedrockAliasCfg struct { + InferenceProfileARN *EnvVar `json:"inference_profile_arn,omitempty"` +} + +// ReplicateAliasCfg holds Replicate-specific overrides that apply to a single alias. +type ReplicateAliasCfg struct { + UseDeploymentsEndpoint *bool `json:"use_deployments_endpoint,omitempty"` +} + +// AliasConfig is the rich value type held by KeyAliases. It carries everything +// needed to call a provider for an aliased model: the wire model identifier +// (ModelID), the canonical model name used for pricing/logging (ModelName), the +// family used for provider routing decisions (ModelFamily), and optional +// provider-specific overrides that override the key-level defaults. +type AliasConfig struct { + ModelID string `json:"model_id"` // wire model identifier sent to the provider + ModelName *string `json:"model_name,omitempty"` // canonical model name used for pricing, logging, and 2nd-tier family routing + ModelFamily *ModelFamily `json:"model_family,omitempty"` // 1st-tier family routing enum + Description string `json:"description,omitempty"` // description of the alias for users to understand its purpose (not used by bifrost) + Region *EnvVar `json:"region,omitempty"` + + *AzureAliasCfg + *VertexAliasCfg + *BedrockAliasCfg + *ReplicateAliasCfg +} + +// isLegacyShape reports whether this AliasConfig carries only ModelID and no +// other fields. Used by MarshalJSON to emit the legacy string-valued wire +// shape so older consumers that expect map[string]string keep working. +func (ac AliasConfig) isLegacyShape() bool { + return ac.ModelID != "" && + ac.ModelName == nil && + ac.ModelFamily == nil && + ac.Description == "" && + ac.Region == nil && + ac.AzureAliasCfg == nil && + ac.VertexAliasCfg == nil && + ac.BedrockAliasCfg == nil && + ac.ReplicateAliasCfg == nil +} + +// MarshalJSON emits the legacy string wire shape when only ModelID is set, so +// callers that haven't opted into the rich AliasConfig see no observable +// change on the wire. When any other field is populated, the full object is +// emitted. +func (ac AliasConfig) MarshalJSON() ([]byte, error) { + if ac.isLegacyShape() { + return Marshal(ac.ModelID) + } + type aliasConfigJSON AliasConfig + return Marshal(aliasConfigJSON(ac)) +} + +// KeyAliases maps a user-facing model name to its AliasConfig. +// +// Both the input (UnmarshalJSON) and the output (AliasConfig.MarshalJSON) +// transparently accept and emit two JSON wire shapes: +// - Legacy: {"my-model": "provider-model-id"} — value is a string +// - New: {"my-model": {"model_id": "provider-model-id", ... }} — value is an object +// +// Legacy entries deserialize to AliasConfig{ModelID: }; an AliasConfig +// that only has ModelID set serializes back to a plain string. This keeps the +// wire format byte-for-byte compatible with the pre-refactor flow until +// ModelName / ModelFamily / provider sub-configs are populated explicitly. +type KeyAliases map[string]AliasConfig func (ka KeyAliases) Validate() error { seen := make(map[string]struct{}, len(ka)) - for from, to := range ka { + for from, ac := range ka { if strings.TrimSpace(from) == "" { return fmt.Errorf("alias source cannot be empty") } - if strings.TrimSpace(to) == "" { - return fmt.Errorf("alias target for %q cannot be empty", from) + if strings.TrimSpace(ac.ModelID) == "" { + return fmt.Errorf("alias %q: model_id cannot be empty", from) } if strings.TrimSpace(from) != from { return fmt.Errorf("alias source %q cannot have leading or trailing whitespace", from) } - if strings.TrimSpace(to) != to { - return fmt.Errorf("alias target for %q cannot have leading or trailing whitespace", from) + if strings.TrimSpace(ac.ModelID) != ac.ModelID { + return fmt.Errorf("alias %q: model_id cannot have leading or trailing whitespace", from) + } + if ac.ModelName != nil && strings.TrimSpace(*ac.ModelName) != *ac.ModelName { + return fmt.Errorf("alias %q: model_name cannot have leading or trailing whitespace", from) + } + if ac.ModelFamily != nil && !ac.ModelFamily.IsValid() { + return fmt.Errorf("alias %q: invalid model_family %q", from, *ac.ModelFamily) } normalized := strings.ToLower(from) if _, ok := seen[normalized]; ok { @@ -167,20 +283,76 @@ func (ka KeyAliases) Validate() error { return nil } +// Resolve returns the wire model identifier for the given user-facing model name. +// If no alias matches, the input is returned unchanged. Case-insensitive fallback +// matches the prior behavior. +// +// This signature is preserved for backward compatibility with existing callers +// that only need the wire model string. For access to the full AliasConfig +// (ModelName, ModelFamily, provider overrides), use ResolveConfig. func (ka KeyAliases) Resolve(model string) string { + if ac := ka.ResolveConfig(model); ac != nil { + return ac.ModelID + } + return model +} + +// 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 { if ka == nil { - return model + return nil } - if alias, ok := ka[model]; ok { - return alias + if ac, ok := ka[model]; ok { + return &ac } - // Fall back to case-insensitive lookup for consistency with WhiteList.Contains for k, v := range ka { if strings.EqualFold(k, model) { - return v + return &v } } - return model + return nil +} + +// UnmarshalJSON accepts both the legacy {"k":"v"} and new {"k":{...}} wire +// shapes for KeyAliases. Legacy string values are promoted to +// AliasConfig{ModelID: }. +func (ka *KeyAliases) UnmarshalJSON(data []byte) error { + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 || string(trimmed) == "null" { + *ka = nil + return nil + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + result := make(KeyAliases, len(raw)) + for k, entry := range raw { + entryTrim := bytes.TrimSpace(entry) + if len(entryTrim) == 0 { + return fmt.Errorf("alias %q: empty value", k) + } + switch entryTrim[0] { + case '"': + // Legacy string value — promote to AliasConfig{ModelID: ...}. + var modelID string + if err := json.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 { + return fmt.Errorf("alias %q: %w", k, err) + } + result[k] = ac + default: + return fmt.Errorf("alias %q: value must be a string (legacy) or object", k) + } + } + *ka = result + return nil } type AzureAuthType string diff --git a/core/schemas/account_test.go b/core/schemas/account_test.go new file mode 100644 index 00000000000..18934bb41b2 --- /dev/null +++ b/core/schemas/account_test.go @@ -0,0 +1,308 @@ +package schemas + +import ( + "encoding/json" + "reflect" + "strings" + "testing" +) + +func TestKeyAliasesUnmarshalLegacyStringShape(t *testing.T) { + in := []byte(`{"best-model": "gpt-4o-deployment"}`) + var ka KeyAliases + if err := json.Unmarshal(in, &ka); err != nil { + t.Fatalf("unmarshal: %v", err) + } + want := KeyAliases{"best-model": AliasConfig{ModelID: "gpt-4o-deployment"}} + if !reflect.DeepEqual(ka, want) { + t.Fatalf("legacy shape mismatch: got %+v, want %+v", ka, want) + } +} + +func TestKeyAliasesUnmarshalRichShape(t *testing.T) { + // Provider sub-configs are embedded, so their fields appear at the top level of the JSON. + in := []byte(`{ + "best-model": { + "model_id": "azure-deployment-xyz", + "model_name": "claude-3-5-sonnet", + "model_family": "anthropic", + "description": "prod", + "api_version": "2024-08-01-preview" + } + }`) + var ka KeyAliases + if err := json.Unmarshal(in, &ka); err != nil { + t.Fatalf("unmarshal: %v", err) + } + got := ka["best-model"] + if got.ModelID != "azure-deployment-xyz" { + t.Fatalf("ModelID mismatch: %q", got.ModelID) + } + if got.ModelName == nil || *got.ModelName != "claude-3-5-sonnet" { + t.Fatalf("ModelName mismatch: %+v", got.ModelName) + } + if got.ModelFamily == nil || *got.ModelFamily != ModelFamilyAnthropic { + t.Fatalf("ModelFamily mismatch: %+v", got.ModelFamily) + } + if got.Description != "prod" { + t.Fatalf("Description mismatch: %q", got.Description) + } + if got.AzureAliasCfg == nil || got.APIVersion == nil || *got.APIVersion != "2024-08-01-preview" { + t.Fatalf("AzureAliasCfg.APIVersion mismatch: %+v", got.AzureAliasCfg) + } +} + +func TestKeyAliasesUnmarshalMixedShape(t *testing.T) { + in := []byte(`{ + "legacy": "gpt-4-deployment", + "rich": {"model_id": "azure-xyz", "model_family": "openai"} + }`) + var ka KeyAliases + if err := json.Unmarshal(in, &ka); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got := ka["legacy"]; got.ModelID != "gpt-4-deployment" || got.ModelFamily != nil { + t.Fatalf("legacy entry wrong: %+v", got) + } + got := ka["rich"] + if got.ModelID != "azure-xyz" || got.ModelFamily == nil || *got.ModelFamily != ModelFamilyOpenAI { + t.Fatalf("rich entry wrong: %+v", got) + } +} + +func TestKeyAliasesUnmarshalEmptyAndNull(t *testing.T) { + cases := map[string]string{ + "empty-obj": `{}`, + "null": `null`, + } + for name, in := range cases { + var ka KeyAliases + if err := json.Unmarshal([]byte(in), &ka); err != nil { + t.Fatalf("%s: unmarshal: %v", name, err) + } + if len(ka) != 0 { + t.Fatalf("%s: want empty/nil, got %+v", name, ka) + } + } +} + +func TestKeyAliasesUnmarshalRoundTrip(t *testing.T) { + orig := KeyAliases{ + "best-model": AliasConfig{ + ModelID: "azure-xyz", + ModelName: Ptr("claude-3-5-sonnet"), + ModelFamily: Ptr(ModelFamilyAnthropic), + AzureAliasCfg: &AzureAliasCfg{ + APIVersion: Ptr("2024-08-01-preview"), + }, + }, + "simple": AliasConfig{ModelID: "gpt-4"}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var back KeyAliases + if err := json.Unmarshal(data, &back); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(orig, back) { + t.Fatalf("round-trip mismatch:\nwant: %+v\ngot: %+v", orig, back) + } +} + +func TestKeyAliasesMarshalLegacyShapeWhenOnlyModelIDSet(t *testing.T) { + // Only ModelID populated — should serialize to the legacy string-valued shape. + ka := KeyAliases{"best-model": AliasConfig{ModelID: "gpt-4o-deployment"}} + data, err := json.Marshal(ka) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if string(data) != `{"best-model":"gpt-4o-deployment"}` { + t.Fatalf("legacy shape mismatch: got %s", data) + } +} + +func TestKeyAliasesMarshalRichShapeWhenAnyExtraFieldSet(t *testing.T) { + cases := map[string]struct { + ac AliasConfig + wantKey string + wantValue any + }{ + "with_model_name": {AliasConfig{ModelID: "x", ModelName: Ptr("canonical")}, "model_name", "canonical"}, + "with_model_family": {AliasConfig{ModelID: "x", ModelFamily: Ptr(ModelFamilyAnthropic)}, "model_family", "anthropic"}, + "with_description": {AliasConfig{ModelID: "x", Description: "prod"}, "description", "prod"}, + "with_azure_subcfg": {AliasConfig{ModelID: "x", AzureAliasCfg: &AzureAliasCfg{APIVersion: Ptr("2024-08-01-preview")}}, "api_version", "2024-08-01-preview"}, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + data, err := json.Marshal(c.ac) + if err != nil { + t.Fatalf("marshal: %v", err) + } + // Rich shape should be a JSON object, not a string. + if len(data) == 0 || data[0] != '{' { + t.Fatalf("want object shape, got %s", data) + } + var out map[string]any + if err := json.Unmarshal(data, &out); err != nil { + t.Fatalf("re-unmarshal: %v", err) + } + got, ok := out[c.wantKey] + if !ok { + t.Fatalf("expected key %q in serialized output, got %s", c.wantKey, data) + } + if got != c.wantValue { + t.Fatalf("field %q: want %v, got %v (raw: %s)", c.wantKey, c.wantValue, got, data) + } + }) + } +} + +func TestKeyAliasesMarshalUnmarshalLegacyRoundTrip(t *testing.T) { + // Legacy in → legacy out: byte-for-byte stable for the unenriched case. + in := []byte(`{"best-model":"gpt-4o-deployment"}`) + var ka KeyAliases + if err := json.Unmarshal(in, &ka); err != nil { + t.Fatalf("unmarshal: %v", err) + } + out, err := json.Marshal(ka) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if string(out) != string(in) { + t.Fatalf("round-trip drift:\n in: %s\nout: %s", in, out) + } +} + +func TestKeyAliasesUnmarshalInvalidValueType(t *testing.T) { + for _, in := range []string{ + `{"k": 123}`, + `{"k": [1,2]}`, + `{"k": true}`, + } { + var ka KeyAliases + if err := json.Unmarshal([]byte(in), &ka); err == nil { + t.Fatalf("expected error for %q, got nil", in) + } + } +} + +func TestKeyAliasesResolveBackwardCompat(t *testing.T) { + ka := KeyAliases{ + "best-model": AliasConfig{ModelID: "gpt-4o-deployment"}, + } + if got := ka.Resolve("best-model"); got != "gpt-4o-deployment" { + t.Fatalf("Resolve mismatch: %q", got) + } + if got := ka.Resolve("BEST-MODEL"); got != "gpt-4o-deployment" { + t.Fatalf("Resolve case-insensitive fallback failed: %q", got) + } + if got := ka.Resolve("unmapped"); got != "unmapped" { + t.Fatalf("Resolve unmatched mismatch: %q", got) + } + var nilKA KeyAliases + if got := nilKA.Resolve("x"); got != "x" { + t.Fatalf("nil Resolve mismatch: %q", got) + } +} + +func TestKeyAliasesResolveConfig(t *testing.T) { + ka := KeyAliases{ + "best-model": AliasConfig{ModelID: "azure-xyz", ModelFamily: Ptr(ModelFamilyAnthropic)}, + } + got := ka.ResolveConfig("best-model") + if got == nil || got.ModelID != "azure-xyz" || got.ModelFamily == nil || *got.ModelFamily != ModelFamilyAnthropic { + t.Fatalf("ResolveConfig mismatch: %+v", got) + } + if ka.ResolveConfig("unmapped") != nil { + t.Fatalf("ResolveConfig should return nil for unmapped") + } +} + +func TestKeyAliasesValidate(t *testing.T) { + madeUp := ModelFamily("made-up") + cases := []struct { + name string + ka KeyAliases + wantErr string + }{ + { + name: "ok", + ka: KeyAliases{"k": {ModelID: "v"}}, + }, + { + name: "empty source", + ka: KeyAliases{"": {ModelID: "v"}}, + wantErr: "alias source cannot be empty", + }, + { + name: "empty model id", + ka: KeyAliases{"k": {ModelID: ""}}, + wantErr: "model_id cannot be empty", + }, + { + name: "whitespace source", + ka: KeyAliases{" k ": {ModelID: "v"}}, + wantErr: "leading or trailing whitespace", + }, + { + name: "whitespace model_id", + ka: KeyAliases{"k": {ModelID: "v "}}, + wantErr: "model_id cannot have leading or trailing whitespace", + }, + { + name: "whitespace model_name", + ka: KeyAliases{"k": {ModelID: "v", ModelName: Ptr(" canonical ")}}, + wantErr: "model_name cannot have leading or trailing whitespace", + }, + { + name: "duplicate source case-insensitive", + ka: KeyAliases{"Key": {ModelID: "v"}, "key": {ModelID: "v"}}, + wantErr: "duplicate alias source", + }, + { + name: "invalid family", + ka: KeyAliases{"k": {ModelID: "v", ModelFamily: &madeUp}}, + wantErr: "invalid model_family", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := c.ka.Validate() + if c.wantErr == "" { + if err != nil { + t.Fatalf("want ok, got %v", err) + } + return + } + if err == nil || !strings.Contains(err.Error(), c.wantErr) { + t.Fatalf("want error containing %q, got %v", c.wantErr, err) + } + }) + } +} + +func TestModelFamilyIsValid(t *testing.T) { + valid := []ModelFamily{ + ModelFamilyAnthropic, ModelFamilyOpenAI, ModelFamilyMistral, + ModelFamilyCohere, ModelFamilyGemini, ModelFamilyNova, ModelFamilyTitan, + } + for _, mf := range valid { + v := mf + if !v.IsValid() { + t.Fatalf("%q should be valid", mf) + } + } + for _, mf := range []ModelFamily{"", "unknown", "claude"} { + v := mf + if v.IsValid() { + t.Fatalf("%q should be invalid", mf) + } + } + // nil receiver is invalid. + var nilMF *ModelFamily + if nilMF.IsValid() { + t.Fatal("nil ModelFamily should be invalid") + } +} diff --git a/framework/configstore/encryption_test.go b/framework/configstore/encryption_test.go index 4b5c553904c..6a795c6fca4 100644 --- a/framework/configstore/encryption_test.go +++ b/framework/configstore/encryption_test.go @@ -775,7 +775,7 @@ func TestEncryptPlaintextKeys_BedrockFields_EncryptsAndDecryptsCorrectly(t *test assert.Equal(t, "us-west-2", found.BedrockKeyConfig.Region.GetValue()) require.NotNil(t, found.BedrockKeyConfig.ARN) assert.Equal(t, "arn:aws:iam::123456789:role/bedrock", found.BedrockKeyConfig.ARN.GetValue()) - assert.Equal(t, "profile-claude", found.Aliases["claude-3"]) + assert.Equal(t, "profile-claude", found.Aliases["claude-3"].ModelID) require.NotNil(t, found.BedrockKeyConfig.BatchS3Config) require.Len(t, found.BedrockKeyConfig.BatchS3Config.Buckets, 1) assert.Equal(t, "my-bucket", found.BedrockKeyConfig.BatchS3Config.Buckets[0].BucketName) diff --git a/framework/configstore/keyhash_alias_test.go b/framework/configstore/keyhash_alias_test.go new file mode 100644 index 00000000000..604ab03fbe6 --- /dev/null +++ b/framework/configstore/keyhash_alias_test.go @@ -0,0 +1,76 @@ +package configstore + +import ( + "testing" + + "github.com/bytedance/sonic" + "github.com/maximhq/bifrost/core/schemas" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGenerateKeyHash_LegacyAliasesPreserveByteShape proves that an +// unenriched alias (only ModelID set) marshals into the hasher as the legacy +// {"k":"v"} string-valued shape, which is what keeps config_hash byte-stable +// across the refactor. If MarshalJSON ever stops emitting the legacy form for +// ModelID-only entries, this test fires. +// +// Strategy: hash the same Key with two equivalent alias representations — the +// rich KeyAliases{"k": {ModelID: "v"}} and (a hand-rolled JSON for) the +// legacy "k": "v" shape — and confirm both feed identical bytes into the +// hasher by checking that the marshaled outputs match. We don't recompute the +// full SHA256 since GenerateKeyHash composes many field bytes; the marshaling +// stability of the alias map alone is the regression-prone surface. +func TestGenerateKeyHash_LegacyAliasesPreserveByteShape(t *testing.T) { + key := schemas.Key{ + Name: "openai-key", + Value: *schemas.NewEnvVar("sk-test"), + Weight: 1.0, + Aliases: schemas.KeyAliases{"best-model": {ModelID: "gpt-4o-deployment"}}, + } + + gotMarshal, err := sonic.Marshal(key.Aliases) + require.NoError(t, err) + assert.Equal(t, + `{"best-model":"gpt-4o-deployment"}`, + string(gotMarshal), + "unenriched alias should marshal to the legacy string-valued wire shape; otherwise GenerateKeyHash drifts from pre-refactor rows", + ) + + // And GenerateKeyHash itself runs cleanly with the new types. + hash, err := GenerateKeyHash(key) + require.NoError(t, err) + assert.NotEmpty(t, hash) +} + +// TestGenerateKeyHash_RichAliasesProduceDifferentHash sanity-checks the other +// side: enriching an alias with ModelName/Family/etc. *does* change the hash, +// so genuine config changes are still detected. +func TestGenerateKeyHash_RichAliasesProduceDifferentHash(t *testing.T) { + canonical := "gpt-4o" + family := schemas.ModelFamilyOpenAI + + legacy := schemas.Key{ + Name: "k", + Value: *schemas.NewEnvVar("sk"), + Weight: 1.0, + Aliases: schemas.KeyAliases{"x": {ModelID: "y"}}, + } + rich := schemas.Key{ + Name: "k", + Value: *schemas.NewEnvVar("sk"), + Weight: 1.0, + Aliases: schemas.KeyAliases{"x": { + ModelID: "y", + ModelName: &canonical, + ModelFamily: &family, + }}, + } + + legacyHash, err := GenerateKeyHash(legacy) + require.NoError(t, err) + richHash, err := GenerateKeyHash(rich) + require.NoError(t, err) + + assert.NotEqual(t, legacyHash, richHash, "enriching an alias must change the key hash so config diffs are detected") +} diff --git a/framework/configstore/migrations_test.go b/framework/configstore/migrations_test.go index f522c7cb955..5a274d533b8 100644 --- a/framework/configstore/migrations_test.go +++ b/framework/configstore/migrations_test.go @@ -1090,8 +1090,8 @@ func TestMigrationDropDeploymentColumnsAndAddAliases_BedrockEncrypted(t *testing // Verify the aliases contain the original deployment data (not double-encrypted) aliases := keys[0].Aliases assert.Contains(t, aliases, "claude") - assert.Equal(t, "dep-claude", aliases["claude"]) - assert.Equal(t, "dep-instant", aliases["claude-instant"]) + assert.Equal(t, "dep-claude", aliases["claude"].ModelID) + assert.Equal(t, "dep-instant", aliases["claude-instant"].ModelID) } // ============================================================================ diff --git a/framework/configstore/tables/encryption_test.go b/framework/configstore/tables/encryption_test.go index 9b329fbe518..2454f5cfd14 100644 --- a/framework/configstore/tables/encryption_test.go +++ b/framework/configstore/tables/encryption_test.go @@ -178,7 +178,7 @@ func TestTableKey_BedrockFieldsEncryptDecrypt(t *testing.T) { Provider: "bedrock", KeyID: "bedrock-uuid-1", Value: *schemas.NewEnvVar("bedrock-val"), - Aliases: schemas.KeyAliases{"model-a": "profile-a"}, + Aliases: schemas.KeyAliases{"model-a": {ModelID: "profile-a"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -224,7 +224,7 @@ func TestTableKey_BedrockFieldsEncryptDecrypt(t *testing.T) { assert.Equal(t, "us-west-2", found.BedrockKeyConfig.Region.GetValue()) require.NotNil(t, found.BedrockKeyConfig.ARN) assert.Equal(t, "arn:aws:iam::123456789:role/test", found.BedrockKeyConfig.ARN.GetValue()) - assert.Equal(t, "profile-a", found.Aliases["model-a"]) + assert.Equal(t, "profile-a", found.Aliases["model-a"].ModelID) require.NotNil(t, found.BedrockKeyConfig.BatchS3Config) require.Len(t, found.BedrockKeyConfig.BatchS3Config.Buckets, 1) assert.Equal(t, "my-batch-bucket", found.BedrockKeyConfig.BatchS3Config.Buckets[0].BucketName) @@ -1156,7 +1156,7 @@ func TestTableKey_AllProviderConfigs_EncryptDecrypt(t *testing.T) { Provider: "custom", KeyID: "multi-uuid", Value: *schemas.NewEnvVar("multi-api-key"), - Aliases: schemas.KeyAliases{"claude-3": "profile-claude"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "profile-claude"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://azure.endpoint.com"), ClientID: schemas.NewEnvVar("multi-azure-cid"), @@ -1230,7 +1230,7 @@ func TestTableKey_AllProviderConfigs_EncryptDecrypt(t *testing.T) { assert.Equal(t, "eu-west-1", found.BedrockKeyConfig.Region.GetValue()) require.NotNil(t, found.BedrockKeyConfig.ARN) assert.Equal(t, "arn:aws:bedrock:eu-west-1:123:role", found.BedrockKeyConfig.ARN.GetValue()) - assert.Equal(t, "profile-claude", found.Aliases["claude-3"]) + assert.Equal(t, "profile-claude", found.Aliases["claude-3"].ModelID) } // ============================================================================ @@ -1919,3 +1919,139 @@ func TestTableKey_VertexPlainValue_RoundTrip(t *testing.T) { assert.False(t, found.VertexKeyConfig.ProjectID.FromEnv) assert.Equal(t, "us-central1", found.VertexKeyConfig.Region.GetValue()) } + +// TestTableKey_AliasesJSON_LegacyWireShape verifies that a KeyAliases value +// containing only ModelID (the unenriched shape) is persisted to the DB as the +// legacy {"k":"v"} string-valued JSON, preserving byte-for-byte wire compat +// with pre-refactor consumers and keeping config_hash stable. +func TestTableKey_AliasesJSON_LegacyWireShape(t *testing.T) { + db := setupTestDB(t) + + key := &TableKey{ + Name: "openai-key", + ProviderID: 1, + Provider: "openai", + KeyID: "openai-uuid-aliases-legacy", + Value: *schemas.NewEnvVar("sk-test"), + Aliases: schemas.KeyAliases{ + "best-model": {ModelID: "gpt-4o-deployment"}, + "backup": {ModelID: "gpt-3.5-turbo"}, + }, + } + require.NoError(t, db.Create(key).Error) + + raw := rawRow(t, db, "config_keys", key.ID) + rawAliasesVal := raw["aliases_json"] + var rawAliasesStr string + switch v := rawAliasesVal.(type) { + case string: + rawAliasesStr = v + case []byte: + rawAliasesStr = string(v) + } + require.NotEmpty(t, rawAliasesStr) + + plaintext, err := encrypt.Decrypt(rawAliasesStr) + require.NoError(t, err, "aliases_json should be decryptable") + + // Both expected shapes are valid JSON encodings (map iteration order is not stable). + candidates := []string{ + `{"best-model":"gpt-4o-deployment","backup":"gpt-3.5-turbo"}`, + `{"backup":"gpt-3.5-turbo","best-model":"gpt-4o-deployment"}`, + } + assert.Contains(t, candidates, plaintext, "legacy ModelID-only aliases should marshal to the string-valued legacy wire shape") +} + +// TestTableKey_AliasesJSON_RichRoundTrip verifies that an enriched AliasConfig +// (with ModelName/ModelFamily/sub-config populated) survives the full DB +// encrypt → save → load → decrypt round-trip with no loss of information. +func TestTableKey_AliasesJSON_RichRoundTrip(t *testing.T) { + db := setupTestDB(t) + + apiVersion := "2024-08-01-preview" + canonical := "claude-3-5-sonnet" + family := schemas.ModelFamilyAnthropic + + key := &TableKey{ + Name: "azure-rich", + ProviderID: 1, + Provider: "azure", + KeyID: "azure-uuid-aliases-rich", + Value: *schemas.NewEnvVar("sk-test"), + Aliases: schemas.KeyAliases{ + "best-model": { + ModelID: "azure-deployment-xyz", + ModelName: &canonical, + ModelFamily: &family, + Description: "prod summarizer", + AzureAliasCfg: &schemas.AzureAliasCfg{ + APIVersion: &apiVersion, + }, + }, + "plain": {ModelID: "gpt-4o-fallback"}, + }, + AzureKeyConfig: &schemas.AzureKeyConfig{ + Endpoint: *schemas.NewEnvVar("https://example.openai.azure.com"), + }, + } + require.NoError(t, db.Create(key).Error) + + var found TableKey + require.NoError(t, db.First(&found, key.ID).Error) + require.NotNil(t, found.Aliases) + require.Len(t, found.Aliases, 2) + + rich := found.Aliases["best-model"] + assert.Equal(t, "azure-deployment-xyz", rich.ModelID) + require.NotNil(t, rich.ModelName) + assert.Equal(t, canonical, *rich.ModelName) + require.NotNil(t, rich.ModelFamily) + assert.Equal(t, schemas.ModelFamilyAnthropic, *rich.ModelFamily) + assert.Equal(t, "prod summarizer", rich.Description) + require.NotNil(t, rich.AzureAliasCfg) + require.NotNil(t, rich.AzureAliasCfg.APIVersion) + assert.Equal(t, apiVersion, *rich.AzureAliasCfg.APIVersion) + + // The unenriched sibling stays a legacy-shape entry — proves marshaling + // only escalates to the rich object form for entries that need it. + plain := found.Aliases["plain"] + assert.Equal(t, "gpt-4o-fallback", plain.ModelID) + assert.Nil(t, plain.ModelName) + assert.Nil(t, plain.ModelFamily) + assert.Nil(t, plain.AzureAliasCfg) +} + +// TestTableKey_AliasesJSON_LegacyInputRoundTrip simulates a row written before +// the refactor — raw legacy {"k":"v"} JSON in the aliases_json column — and +// verifies AfterFind promotes it to AliasConfig{ModelID: v} transparently. +func TestTableKey_AliasesJSON_LegacyInputRoundTrip(t *testing.T) { + db := setupTestDB(t) + + // First create a key without aliases so the row exists. + key := &TableKey{ + Name: "openai-key", + ProviderID: 1, + Provider: "openai", + KeyID: "openai-uuid-aliases-legacy-input", + Value: *schemas.NewEnvVar("sk-test"), + } + require.NoError(t, db.Create(key).Error) + + // Then write the legacy-shaped JSON directly into the aliases_json column, + // bypassing BeforeSave — this is what a pre-refactor row looks like. + legacy := `{"best-model":"gpt-4o-deployment"}` + encrypted, err := encrypt.Encrypt(legacy) + require.NoError(t, err) + require.NoError(t, db.Exec("UPDATE config_keys SET aliases_json = ? WHERE id = ?", encrypted, key.ID).Error) + + // Read back through GORM — AfterFind should decrypt + UnmarshalJSON should + // promote the legacy string value into AliasConfig{ModelID: ...}. + var found TableKey + require.NoError(t, db.First(&found, key.ID).Error) + require.NotNil(t, found.Aliases) + require.Len(t, found.Aliases, 1) + got := found.Aliases["best-model"] + assert.Equal(t, "gpt-4o-deployment", got.ModelID) + assert.Nil(t, got.ModelName) + assert.Nil(t, got.ModelFamily) +} diff --git a/transports/bifrost-http/lib/config_test.go b/transports/bifrost-http/lib/config_test.go index e9429c823a7..77d06923928 100644 --- a/transports/bifrost-http/lib/config_test.go +++ b/transports/bifrost-http/lib/config_test.go @@ -2695,7 +2695,7 @@ func TestGenerateKeyHash(t *testing.T) { Value: *schemas.NewEnvVar("sk-123"), Models: []string{"gpt-4", "gpt-3.5-turbo"}, Weight: 1.5, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, } hashWithAliases, err := configstore.GenerateKeyHash(keyWithAliases) @@ -5139,7 +5139,7 @@ func TestKeyHashComparison_AzureConfigSyncScenarios(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5150,7 +5150,7 @@ func TestKeyHashComparison_AzureConfigSyncScenarios(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5172,7 +5172,7 @@ func TestKeyHashComparison_AzureConfigSyncScenarios(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5183,7 +5183,7 @@ func TestKeyHashComparison_AzureConfigSyncScenarios(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://different-azure.openai.azure.com"), // Changed! }, @@ -5205,7 +5205,7 @@ func TestKeyHashComparison_AzureConfigSyncScenarios(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5216,7 +5216,7 @@ func TestKeyHashComparison_AzureConfigSyncScenarios(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment", "gpt-3.5-turbo": "gpt-35-turbo-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}, "gpt-3.5-turbo": {ModelID: "gpt-35-turbo-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5304,7 +5304,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5317,7 +5317,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5341,7 +5341,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5354,7 +5354,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAI44QH8DHBEXAMPLE"), // Changed! SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5378,7 +5378,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5391,7 +5391,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("differentSecretKey/NEWKEY/bPxRfiCYEXAMPLEKEY"), // Changed! @@ -5415,7 +5415,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5428,7 +5428,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5452,7 +5452,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5466,7 +5466,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5491,7 +5491,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5504,7 +5504,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile", "claude-3.5": "claude-35-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}, "claude-3.5": {ModelID: "claude-35-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5594,7 +5594,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5608,7 +5608,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5634,7 +5634,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar(""), // Empty for IAM role auth SecretKey: *schemas.NewEnvVar(""), // Empty for IAM role auth @@ -5648,7 +5648,7 @@ func TestKeyHashComparison_BedrockConfigSyncScenarios(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "claude-3-inference-profile"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "claude-3-inference-profile"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5674,7 +5674,7 @@ func TestProviderHashComparison_AzureProviderFullLifecycle(t *testing.T) { Name: "azure-openai-key", Value: *schemas.NewEnvVar("azure-api-key-initial"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5708,7 +5708,7 @@ func TestProviderHashComparison_AzureProviderFullLifecycle(t *testing.T) { Name: "azure-openai-key", Value: *schemas.NewEnvVar("azure-api-key-dashboard-edited"), // Changed via dashboard! Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5740,7 +5740,7 @@ func TestProviderHashComparison_AzureProviderFullLifecycle(t *testing.T) { Name: "azure-openai-key", Value: *schemas.NewEnvVar("azure-api-key-initial"), // Original value from file Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -5776,7 +5776,7 @@ func TestProviderHashComparison_AzureProviderFullLifecycle(t *testing.T) { Name: "azure-openai-key", Value: *schemas.NewEnvVar("azure-api-key-initial"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment", "gpt-4o": "gpt-4o-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}, "gpt-4o": {ModelID: "gpt-4o-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://new-azure.openai.azure.com"), // Changed! }, @@ -5882,7 +5882,7 @@ func TestProviderHashComparison_BedrockProviderFullLifecycle(t *testing.T) { Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), // Empty for Bedrock with IAM or AccessKey auth Weight: 1, - Aliases: schemas.KeyAliases{"claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3-sonnet": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5917,7 +5917,7 @@ func TestProviderHashComparison_BedrockProviderFullLifecycle(t *testing.T) { Name: "aws-bedrock-key-eu", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3-sonnet": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAI44QH8DHBEXAMPLE"), SecretKey: *schemas.NewEnvVar("je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY"), @@ -5944,7 +5944,7 @@ func TestProviderHashComparison_BedrockProviderFullLifecycle(t *testing.T) { Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3-sonnet": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -5983,7 +5983,7 @@ func TestProviderHashComparison_BedrockProviderFullLifecycle(t *testing.T) { Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0", "claude-3-opus": "anthropic.claude-3-opus-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3-sonnet": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}, "claude-3-opus": {ModelID: "anthropic.claude-3-opus-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -6111,7 +6111,7 @@ func TestProviderHashComparison_BedrockProviderFullLifecycle(t *testing.T) { Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0", "claude-3-opus": "anthropic.claude-3-opus-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3-sonnet": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}, "claude-3-opus": {ModelID: "anthropic.claude-3-opus-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -6154,7 +6154,7 @@ func TestProviderHashComparison_AzureNewProviderFromConfig(t *testing.T) { Name: "azure-openai-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -6220,7 +6220,7 @@ func TestProviderHashComparison_BedrockNewProviderFromConfig(t *testing.T) { Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "anthropic.claude-3-sonnet-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -6289,7 +6289,7 @@ func TestProviderHashComparison_AzureDBValuePreservedWhenHashMatches(t *testing. Name: "azure-openai-key", Value: *schemas.NewEnvVar("DASHBOARD-EDITED-SECRET-KEY"), // Dashboard edited this! Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -6317,7 +6317,7 @@ func TestProviderHashComparison_AzureDBValuePreservedWhenHashMatches(t *testing. Name: "azure-openai-key", Value: *schemas.NewEnvVar("original-key-from-file"), // Different value than DB! Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), // Same }, @@ -6373,7 +6373,7 @@ func TestProviderHashComparison_BedrockDBValuePreservedWhenHashMatches(t *testin Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "anthropic.claude-3-sonnet-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("DASHBOARD-EDITED-ACCESS-KEY"), // Dashboard edited! SecretKey: *schemas.NewEnvVar("DASHBOARD-EDITED-SECRET-KEY"), // Dashboard edited! @@ -6403,7 +6403,7 @@ func TestProviderHashComparison_BedrockDBValuePreservedWhenHashMatches(t *testin Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "anthropic.claude-3-sonnet-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "anthropic.claude-3-sonnet-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), // Different! SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), // Different! @@ -6491,7 +6491,7 @@ func TestProviderHashComparison_AzureConfigChangedInFile(t *testing.T) { Name: "azure-openai-key", Value: *schemas.NewEnvVar("azure-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4o": "gpt-4o-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4o": {ModelID: "gpt-4o-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://NEW-azure.openai.azure.com"), // Changed! }, @@ -6575,7 +6575,7 @@ func TestProviderHashComparison_BedrockConfigChangedInFile(t *testing.T) { Name: "aws-bedrock-key", Value: *schemas.NewEnvVar(""), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3-opus": "anthropic.claude-3-opus-20240229-v1:0"}, + Aliases: schemas.KeyAliases{"claude-3-opus": {ModelID: "anthropic.claude-3-opus-20240229-v1:0"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), @@ -14037,7 +14037,7 @@ func TestGenerateKeyHash_RuntimeVsMigrationParity(t *testing.T) { Value: *schemas.NewEnvVar("azure-key-value"), Weight: ptrFloat64(1.0), AzureKeyConfig: azureConfig, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, } schemaKey := schemas.Key{ @@ -14989,7 +14989,7 @@ func TestKeyHashComparison_VertexConfigSyncScenarios(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project-123"), ProjectNumber: *schemas.NewEnvVar("123456789"), @@ -15003,7 +15003,7 @@ func TestKeyHashComparison_VertexConfigSyncScenarios(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project-123"), ProjectNumber: *schemas.NewEnvVar("123456789"), @@ -15133,7 +15133,7 @@ func TestKeyHashComparison_VertexConfigSyncScenarios(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project-123"), Region: *schemas.NewEnvVar("us-central1"), @@ -15145,7 +15145,7 @@ func TestKeyHashComparison_VertexConfigSyncScenarios(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-api-key-123"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint", "gemini-1.5-pro": "gemini-15-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}, "gemini-1.5-pro": {ModelID: "gemini-15-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project-123"), Region: *schemas.NewEnvVar("us-central1"), @@ -15532,7 +15532,7 @@ func TestKeyHashComparison_AzureDeploymentsChange(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -15543,7 +15543,7 @@ func TestKeyHashComparison_AzureDeploymentsChange(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment", "gpt-4o": "gpt-4o-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}, "gpt-4o": {ModelID: "gpt-4o-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -15563,7 +15563,7 @@ func TestKeyHashComparison_AzureDeploymentsChange(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment", "gpt-4o": "gpt-4o-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}, "gpt-4o": {ModelID: "gpt-4o-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -15574,7 +15574,7 @@ func TestKeyHashComparison_AzureDeploymentsChange(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -15594,7 +15594,7 @@ func TestKeyHashComparison_AzureDeploymentsChange(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment-v1"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment-v1"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -15605,7 +15605,7 @@ func TestKeyHashComparison_AzureDeploymentsChange(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment-v2"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment-v2"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -15635,7 +15635,7 @@ func TestKeyHashComparison_AzureDeploymentsChange(t *testing.T) { Name: "azure-key", Value: *schemas.NewEnvVar("azure-api-key"), Weight: 1, - Aliases: schemas.KeyAliases{"gpt-4": "gpt-4-deployment"}, + Aliases: schemas.KeyAliases{"gpt-4": {ModelID: "gpt-4-deployment"}}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://myazure.openai.azure.com"), }, @@ -15658,7 +15658,7 @@ func TestKeyHashComparison_BedrockDeploymentsChange(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-key"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "arn:aws:bedrock:us-east-1::inference-profile/claude-3"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI"), @@ -15671,7 +15671,7 @@ func TestKeyHashComparison_BedrockDeploymentsChange(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-key"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "arn:aws:bedrock:us-east-1::inference-profile/claude-3", "claude-3.5": "arn:aws:bedrock:us-east-1::inference-profile/claude-3.5"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3"}, "claude-3.5": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3.5"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI"), @@ -15693,7 +15693,7 @@ func TestKeyHashComparison_BedrockDeploymentsChange(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-key"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "arn:aws:bedrock:us-east-1::inference-profile/claude-3", "claude-3.5": "arn:aws:bedrock:us-east-1::inference-profile/claude-3.5"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3"}, "claude-3.5": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3.5"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI"), @@ -15706,7 +15706,7 @@ func TestKeyHashComparison_BedrockDeploymentsChange(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-key"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "arn:aws:bedrock:us-east-1::inference-profile/claude-3"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI"), @@ -15728,7 +15728,7 @@ func TestKeyHashComparison_BedrockDeploymentsChange(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-key"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "arn:aws:bedrock:us-east-1::inference-profile/claude-3-old"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3-old"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI"), @@ -15741,7 +15741,7 @@ func TestKeyHashComparison_BedrockDeploymentsChange(t *testing.T) { Name: "bedrock-key", Value: *schemas.NewEnvVar("bedrock-key"), Weight: 1, - Aliases: schemas.KeyAliases{"claude-3": "arn:aws:bedrock:us-east-1::inference-profile/claude-3-new"}, + Aliases: schemas.KeyAliases{"claude-3": {ModelID: "arn:aws:bedrock:us-east-1::inference-profile/claude-3-new"}}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("AKIAIOSFODNN7EXAMPLE"), SecretKey: *schemas.NewEnvVar("wJalrXUtnFEMI"), @@ -15766,7 +15766,7 @@ func TestKeyHashComparison_VertexDeploymentsChange(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-creds"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project"), Region: *schemas.NewEnvVar("us-central1"), @@ -15778,7 +15778,7 @@ func TestKeyHashComparison_VertexDeploymentsChange(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-creds"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint", "gemini-1.5-pro": "gemini-15-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}, "gemini-1.5-pro": {ModelID: "gemini-15-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project"), Region: *schemas.NewEnvVar("us-central1"), @@ -15799,7 +15799,7 @@ func TestKeyHashComparison_VertexDeploymentsChange(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-creds"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint", "gemini-1.5-pro": "gemini-15-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}, "gemini-1.5-pro": {ModelID: "gemini-15-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project"), Region: *schemas.NewEnvVar("us-central1"), @@ -15811,7 +15811,7 @@ func TestKeyHashComparison_VertexDeploymentsChange(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-creds"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project"), Region: *schemas.NewEnvVar("us-central1"), @@ -15832,7 +15832,7 @@ func TestKeyHashComparison_VertexDeploymentsChange(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-creds"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint-v1"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint-v1"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project"), Region: *schemas.NewEnvVar("us-central1"), @@ -15844,7 +15844,7 @@ func TestKeyHashComparison_VertexDeploymentsChange(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-creds"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint-v2"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint-v2"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project"), Region: *schemas.NewEnvVar("us-central1"), @@ -15876,7 +15876,7 @@ func TestKeyHashComparison_VertexDeploymentsChange(t *testing.T) { Name: "vertex-key", Value: *schemas.NewEnvVar("vertex-creds"), Weight: 1, - Aliases: schemas.KeyAliases{"gemini-pro": "gemini-pro-endpoint"}, + Aliases: schemas.KeyAliases{"gemini-pro": {ModelID: "gemini-pro-endpoint"}}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("my-project"), Region: *schemas.NewEnvVar("us-central1"), diff --git a/transports/config.schema.json b/transports/config.schema.json index a28081efb96..f4d9a95a34d 100644 --- a/transports/config.schema.json +++ b/transports/config.schema.json @@ -2398,13 +2398,74 @@ "aliases": { "type": "object", "additionalProperties": { - "type": "string", - "minLength": 1 + "oneOf": [ + { + "type": "string", + "minLength": 1, + "description": "Legacy shape: a bare provider-specific identifier. Equivalent to {\"model_id\": \"\"}." + }, + { + "type": "object", + "properties": { + "model_id": { + "type": "string", + "minLength": 1, + "description": "Provider-specific identifier sent on the wire (deployment name, inference profile ID, fine-tuned model ID, etc.)." + }, + "model_name": { + "type": "string", + "description": "Canonical model name used for pricing, logging, and family inference." + }, + "model_family": { + "type": "string", + "enum": ["anthropic", "openai", "mistral", "cohere", "gemini", "nova", "titan"], + "description": "Underlying model family. Used by provider routing without substring-sniffing the wire model ID." + }, + "description": { + "type": "string" + }, + "region": { + "type": "string", + "description": "Per-alias region override (can use env. prefix)." + }, + "api_version": { + "type": "string", + "description": "Azure OpenAI api-version override for this alias." + }, + "anthropic_version": { + "type": "string", + "description": "Azure anthropic-version header override for Claude-on-Azure deployments." + }, + "endpoint": { + "type": "string", + "description": "Per-alias Azure endpoint override (can use env. prefix)." + }, + "project_id": { + "type": "string", + "description": "Per-alias Vertex project ID override (can use env. prefix)." + }, + "project_number": { + "type": "string", + "description": "Per-alias Vertex project number override (can use env. prefix)." + }, + "inference_profile_arn": { + "type": "string", + "description": "Per-alias Bedrock inference profile ARN (can use env. prefix)." + }, + "use_deployments_endpoint": { + "type": "boolean", + "description": "Replicate: use the deployments endpoint instead of the predictions endpoint for this alias." + } + }, + "required": ["model_id"], + "additionalProperties": false + } + ] }, "propertyNames": { "minLength": 1 }, - "description": "Model alias mappings: maps a model name to a provider-specific identifier (deployment name, inference profile ID, fine-tuned model ID, etc.)" + "description": "Model alias mappings: each entry maps a user-facing model name to either a bare provider identifier (legacy string shape) or an AliasConfig object carrying the wire identifier plus optional canonical name, family, and provider-specific overrides." } }, "required": ["name", "weight"]