fix(billing): 修正模型计费配置继承优先级
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package store
|
||||
|
||||
// EffectiveBillingConfigInput describes the billing layers used by runtime and
|
||||
// catalog responses. LegacyPlatformModelConfig is retained only as a fallback
|
||||
// for models that do not have an effective pricing rule set.
|
||||
type EffectiveBillingConfigInput struct {
|
||||
BaseConfig map[string]any
|
||||
LegacyPlatformModelConfig map[string]any
|
||||
InheritedRuleSetConfig map[string]any
|
||||
ModelRuleSetConfig map[string]any
|
||||
Override map[string]any
|
||||
}
|
||||
|
||||
// ResolveEffectiveBillingConfig keeps inherited pricing rules authoritative over
|
||||
// the legacy materialized snapshot. Explicit model rules and overrides retain
|
||||
// their higher-priority exception semantics.
|
||||
func ResolveEffectiveBillingConfig(input EffectiveBillingConfigInput) map[string]any {
|
||||
config := mergeObjects(input.BaseConfig, nil)
|
||||
if len(input.InheritedRuleSetConfig) > 0 {
|
||||
config = mergeObjects(input.InheritedRuleSetConfig, nil)
|
||||
} else if len(input.LegacyPlatformModelConfig) > 0 {
|
||||
config = mergeObjects(input.LegacyPlatformModelConfig, nil)
|
||||
}
|
||||
if len(input.ModelRuleSetConfig) > 0 {
|
||||
config = mergeObjects(input.ModelRuleSetConfig, nil)
|
||||
}
|
||||
return mergeObjects(config, input.Override)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveEffectiveBillingConfigKeepsPricingRulesAuthoritative(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input EffectiveBillingConfigInput
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "inherited rule replaces stale platform snapshot",
|
||||
input: EffectiveBillingConfigInput{
|
||||
BaseConfig: videoBillingConfig(100),
|
||||
LegacyPlatformModelConfig: videoBillingConfig(100),
|
||||
InheritedRuleSetConfig: videoBillingConfig(416),
|
||||
},
|
||||
want: 416,
|
||||
},
|
||||
{
|
||||
name: "legacy snapshot remains a fallback without a rule",
|
||||
input: EffectiveBillingConfigInput{
|
||||
BaseConfig: videoBillingConfig(100),
|
||||
LegacyPlatformModelConfig: videoBillingConfig(125),
|
||||
},
|
||||
want: 125,
|
||||
},
|
||||
{
|
||||
name: "model rule remains an explicit pricing exception",
|
||||
input: EffectiveBillingConfigInput{
|
||||
BaseConfig: videoBillingConfig(100),
|
||||
LegacyPlatformModelConfig: videoBillingConfig(125),
|
||||
InheritedRuleSetConfig: videoBillingConfig(416),
|
||||
ModelRuleSetConfig: videoBillingConfig(500),
|
||||
},
|
||||
want: 500,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := ResolveEffectiveBillingConfig(test.input)
|
||||
video, ok := config["video"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected video billing config, got %#v", config)
|
||||
}
|
||||
if got := video["basePrice"]; got != test.want {
|
||||
t.Fatalf("video base price = %#v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEffectiveBillingConfigAppliesOverrideLast(t *testing.T) {
|
||||
config := ResolveEffectiveBillingConfig(EffectiveBillingConfigInput{
|
||||
InheritedRuleSetConfig: videoBillingConfig(416),
|
||||
Override: videoBillingConfig(600),
|
||||
})
|
||||
video, ok := config["video"].(map[string]any)
|
||||
if !ok || video["basePrice"] != float64(600) {
|
||||
t.Fatalf("expected override price 600, got %#v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func videoBillingConfig(basePrice float64) map[string]any {
|
||||
return map[string]any{
|
||||
"video": map[string]any{"basePrice": basePrice},
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type modelCatalogSnapshot struct {
|
||||
DisplayName string
|
||||
Capabilities map[string]any
|
||||
BaseBillingConfig map[string]any
|
||||
PricingRuleSetID string
|
||||
DefaultRateLimitPolicy map[string]any
|
||||
RuntimePolicySetID string
|
||||
RuntimePolicyOverride map[string]any
|
||||
@@ -121,10 +122,10 @@ func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier,
|
||||
if err := validateEnabledVolcesTextModelCapabilities(ctx, q, input, capabilities); err != nil {
|
||||
return PlatformModel{}, err
|
||||
}
|
||||
// billing_config is a legacy, explicitly supplied compatibility field. Do
|
||||
// not materialize base-model pricing into it: copied prices become stale as
|
||||
// soon as the base pricing rule changes and can mask the authoritative rule.
|
||||
billingConfig := input.BillingConfig
|
||||
if len(billingConfig) == 0 {
|
||||
billingConfig = mergeObjects(base.BaseBillingConfig, input.BillingConfigOverride)
|
||||
}
|
||||
explicitRuntimePolicySetID := strings.TrimSpace(input.RuntimePolicySetID)
|
||||
rateLimitPolicy := input.RateLimitPolicy
|
||||
if len(rateLimitPolicy) == 0 && explicitRuntimePolicySetID == "" {
|
||||
@@ -260,6 +261,8 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
model.ModelType = decodeStringArray(modelTypeBytes)
|
||||
model.BillingConfigOverride = decodeObject(billingOverrideBytes)
|
||||
model.BillingConfig = decodeObject(billingBytes)
|
||||
model.BaseBillingConfig = base.BaseBillingConfig
|
||||
model.BasePricingRuleSetID = base.PricingRuleSetID
|
||||
model.PermissionConfig = decodeObject(permissionBytes)
|
||||
model.RetryPolicy = decodeObject(retryPolicyBytes)
|
||||
model.RateLimitPolicy = decodeObject(rateLimitPolicyBytes)
|
||||
@@ -368,7 +371,7 @@ func (s *Store) lookupBaseModel(ctx context.Context, q platformModelQuerier, id
|
||||
var modelTypeBytes []byte
|
||||
err := q.QueryRow(ctx, `
|
||||
SELECT id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
|
||||
capabilities, base_billing_config, default_rate_limit_policy,
|
||||
capabilities, base_billing_config, COALESCE(pricing_rule_set_id::text, ''), default_rate_limit_policy,
|
||||
COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override
|
||||
FROM base_model_catalog
|
||||
WHERE ($1 <> '' AND id = NULLIF($1, '')::uuid)
|
||||
@@ -384,6 +387,7 @@ LIMIT 1`, strings.TrimSpace(id), strings.TrimSpace(canonicalKey), strings.TrimSp
|
||||
&item.DisplayName,
|
||||
&capabilities,
|
||||
&billingConfig,
|
||||
&item.PricingRuleSetID,
|
||||
&rateLimitPolicy,
|
||||
&item.RuntimePolicySetID,
|
||||
&runtimePolicyOverride,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListModelsLoadsEffectiveBillingSources(t *testing.T) {
|
||||
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
|
||||
if databaseURL == "" {
|
||||
t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run the platform-model billing source integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
db, err := Connect(ctx, databaseURL)
|
||||
if err != nil {
|
||||
t.Fatalf("connect store: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
models, err := db.ListModels(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list models with effective billing sources: %v", err)
|
||||
}
|
||||
for _, model := range models {
|
||||
if model.BaseModelID == "" {
|
||||
continue
|
||||
}
|
||||
if model.BaseBillingConfig == nil {
|
||||
t.Fatalf("platform model %s did not load base billing config", model.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Skip("database has no base-model-backed platform model")
|
||||
}
|
||||
@@ -217,33 +217,36 @@ type CreatedAPIKey struct {
|
||||
}
|
||||
|
||||
type PlatformModel struct {
|
||||
ID string `json:"id"`
|
||||
PlatformID string `json:"platformId"`
|
||||
BaseModelID string `json:"baseModelId,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
PlatformName string `json:"platformName,omitempty"`
|
||||
ModelName string `json:"modelName"`
|
||||
ProviderModelName string `json:"providerModelName,omitempty"`
|
||||
ModelAlias string `json:"modelAlias,omitempty"`
|
||||
ModelType StringList `json:"modelType"`
|
||||
DisplayName string `json:"displayName"`
|
||||
CapabilityOverride map[string]any `json:"capabilityOverride,omitempty"`
|
||||
Capabilities map[string]any `json:"capabilities,omitempty"`
|
||||
BaseCapabilities map[string]any `json:"-"`
|
||||
PricingMode string `json:"pricingMode"`
|
||||
DiscountFactor float64 `json:"discountFactor,omitempty"`
|
||||
PricingRuleSetID string `json:"pricingRuleSetId,omitempty"`
|
||||
BillingConfigOverride map[string]any `json:"billingConfigOverride,omitempty"`
|
||||
BillingConfig map[string]any `json:"billingConfig,omitempty"`
|
||||
PermissionConfig map[string]any `json:"permissionConfig,omitempty"`
|
||||
RetryPolicy map[string]any `json:"retryPolicy,omitempty"`
|
||||
RateLimitPolicy map[string]any `json:"rateLimitPolicy,omitempty"`
|
||||
RuntimePolicySetID string `json:"runtimePolicySetId,omitempty"`
|
||||
RuntimePolicyOverride map[string]any `json:"runtimePolicyOverride,omitempty"`
|
||||
CooldownUntil string `json:"cooldownUntil,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
PlatformID string `json:"platformId"`
|
||||
BaseModelID string `json:"baseModelId,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
PlatformName string `json:"platformName,omitempty"`
|
||||
ModelName string `json:"modelName"`
|
||||
ProviderModelName string `json:"providerModelName,omitempty"`
|
||||
ModelAlias string `json:"modelAlias,omitempty"`
|
||||
ModelType StringList `json:"modelType"`
|
||||
DisplayName string `json:"displayName"`
|
||||
CapabilityOverride map[string]any `json:"capabilityOverride,omitempty"`
|
||||
Capabilities map[string]any `json:"capabilities,omitempty"`
|
||||
BaseCapabilities map[string]any `json:"-"`
|
||||
BaseBillingConfig map[string]any `json:"-"`
|
||||
BasePricingRuleSetID string `json:"-"`
|
||||
PlatformPricingRuleSetID string `json:"-"`
|
||||
PricingMode string `json:"pricingMode"`
|
||||
DiscountFactor float64 `json:"discountFactor,omitempty"`
|
||||
PricingRuleSetID string `json:"pricingRuleSetId,omitempty"`
|
||||
BillingConfigOverride map[string]any `json:"billingConfigOverride,omitempty"`
|
||||
BillingConfig map[string]any `json:"billingConfig,omitempty"`
|
||||
PermissionConfig map[string]any `json:"permissionConfig,omitempty"`
|
||||
RetryPolicy map[string]any `json:"retryPolicy,omitempty"`
|
||||
RateLimitPolicy map[string]any `json:"rateLimitPolicy,omitempty"`
|
||||
RuntimePolicySetID string `json:"runtimePolicySetId,omitempty"`
|
||||
RuntimePolicyOverride map[string]any `json:"runtimePolicyOverride,omitempty"`
|
||||
CooldownUntil string `json:"cooldownUntil,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type AccessRule struct {
|
||||
@@ -927,7 +930,9 @@ func (s *Store) listModels(ctx context.Context, platformID string) ([]PlatformMo
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT m.id::text, m.platform_id::text, COALESCE(m.base_model_id::text, ''), p.provider, p.name,
|
||||
m.model_name, COALESCE(NULLIF(m.provider_model_name, ''), m.model_name), COALESCE(m.model_alias, ''), m.model_type, m.display_name,
|
||||
m.capability_override, m.capabilities, COALESCE(b.capabilities, '{}'::jsonb), m.pricing_mode, COALESCE(m.discount_factor, 0)::float8,
|
||||
m.capability_override, m.capabilities, COALESCE(b.capabilities, '{}'::jsonb),
|
||||
COALESCE(b.base_billing_config, '{}'::jsonb), COALESCE(b.pricing_rule_set_id::text, ''),
|
||||
COALESCE(p.pricing_rule_set_id::text, ''), m.pricing_mode, COALESCE(m.discount_factor, 0)::float8,
|
||||
COALESCE(m.pricing_rule_set_id::text, ''), m.billing_config_override, m.billing_config,
|
||||
m.permission_config, m.retry_policy, m.rate_limit_policy, COALESCE(m.runtime_policy_set_id::text, ''), m.runtime_policy_override,
|
||||
COALESCE(to_char(m.cooldown_until AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), ''),
|
||||
@@ -935,7 +940,7 @@ SELECT m.id::text, m.platform_id::text, COALESCE(m.base_model_id::text, ''), p.p
|
||||
FROM platform_models m
|
||||
JOIN integration_platforms p ON p.id = m.platform_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT catalog.capabilities
|
||||
SELECT catalog.capabilities, catalog.base_billing_config, catalog.pricing_rule_set_id
|
||||
FROM base_model_catalog catalog
|
||||
WHERE (m.base_model_id IS NOT NULL AND catalog.id = m.base_model_id)
|
||||
OR (
|
||||
@@ -962,6 +967,7 @@ ORDER BY m.model_type ASC, m.model_name ASC`, args...)
|
||||
var capabilityOverride []byte
|
||||
var capabilities []byte
|
||||
var baseCapabilities []byte
|
||||
var baseBillingConfig []byte
|
||||
var billingConfigOverride []byte
|
||||
var billingConfig []byte
|
||||
var permissionConfig []byte
|
||||
@@ -983,6 +989,9 @@ ORDER BY m.model_type ASC, m.model_name ASC`, args...)
|
||||
&capabilityOverride,
|
||||
&capabilities,
|
||||
&baseCapabilities,
|
||||
&baseBillingConfig,
|
||||
&model.BasePricingRuleSetID,
|
||||
&model.PlatformPricingRuleSetID,
|
||||
&model.PricingMode,
|
||||
&model.DiscountFactor,
|
||||
&model.PricingRuleSetID,
|
||||
@@ -1003,6 +1012,7 @@ ORDER BY m.model_type ASC, m.model_name ASC`, args...)
|
||||
model.CapabilityOverride = decodeObject(capabilityOverride)
|
||||
model.Capabilities = decodeObject(capabilities)
|
||||
model.BaseCapabilities = decodeObject(baseCapabilities)
|
||||
model.BaseBillingConfig = decodeObject(baseBillingConfig)
|
||||
model.ModelType = decodeStringArray(modelTypeBytes)
|
||||
model.BillingConfigOverride = decodeObject(billingConfigOverride)
|
||||
model.BillingConfig = decodeObject(billingConfig)
|
||||
|
||||
Reference in New Issue
Block a user