mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-27 08:05:33 +00:00
refactor(grok): 将视频模型价纳入 Ent 持久化
This commit is contained in:
@@ -969,6 +969,7 @@ func groupEntityToService(g *dbent.Group) *service.Group {
|
||||
VideoPrice480P: g.VideoPrice480p,
|
||||
VideoPrice720P: g.VideoPrice720p,
|
||||
VideoPrice1080P: g.VideoPrice1080p,
|
||||
VideoModelPrices: service.NormalizeVideoModelPrices(g.VideoModelPrices),
|
||||
WebSearchPricePerCall: g.WebSearchPricePerCall,
|
||||
DefaultValidityDays: g.DefaultValidityDays,
|
||||
ClaudeCodeOnly: g.ClaudeCodeOnly,
|
||||
|
||||
@@ -19,6 +19,9 @@ func TestGroupEntityToService_PreservesMessagesDispatchModelConfig(t *testing.T)
|
||||
RateMultiplier: 1,
|
||||
AllowMessagesDispatch: true,
|
||||
DefaultMappedModel: "gpt-5.4",
|
||||
VideoModelPrices: map[string]map[string]float64{
|
||||
service.VideoPriceFamilyGrokImagineVideo15: {service.VideoBillingResolution720P: 0.14},
|
||||
},
|
||||
MessagesDispatchModelConfig: service.OpenAIMessagesDispatchModelConfig{
|
||||
OpusMappedModel: "gpt-5.4-nano",
|
||||
SonnetMappedModel: "gpt-5.3-codex",
|
||||
@@ -32,6 +35,7 @@ func TestGroupEntityToService_PreservesMessagesDispatchModelConfig(t *testing.T)
|
||||
got := groupEntityToService(group)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, group.MessagesDispatchModelConfig, got.MessagesDispatchModelConfig)
|
||||
require.Equal(t, group.VideoModelPrices, got.VideoModelPrices)
|
||||
}
|
||||
|
||||
func TestAPIKeyRepository_GetByKeyForAuth_PreservesMessagesDispatchModelConfig_SQLite(t *testing.T) {
|
||||
|
||||
@@ -46,9 +46,6 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er
|
||||
if err := createGroupRecord(ctx, r.client, groupIn); err != nil {
|
||||
return err
|
||||
}
|
||||
if saveErr := saveGroupVideoModelPrices(ctx, r.sql, groupIn.ID, groupIn.VideoModelPrices); saveErr != nil {
|
||||
return fmt.Errorf("save group video_model_prices: %w", saveErr)
|
||||
}
|
||||
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventGroupChanged, nil, &groupIn.ID, nil); err != nil {
|
||||
logger.LegacyPrintf("repository.group", "[SchedulerOutbox] enqueue group create failed: group=%d err=%v", groupIn.ID, err)
|
||||
}
|
||||
@@ -85,6 +82,7 @@ func createGroupRecord(ctx context.Context, client *dbent.Client, groupIn *servi
|
||||
SetNillableVideoPrice480p(groupIn.VideoPrice480P).
|
||||
SetNillableVideoPrice720p(groupIn.VideoPrice720P).
|
||||
SetNillableVideoPrice1080p(groupIn.VideoPrice1080P).
|
||||
SetVideoModelPrices(service.NormalizeVideoModelPrices(groupIn.VideoModelPrices)).
|
||||
SetNillableWebSearchPricePerCall(groupIn.WebSearchPricePerCall).
|
||||
SetDefaultValidityDays(groupIn.DefaultValidityDays).
|
||||
SetClaudeCodeOnly(groupIn.ClaudeCodeOnly).
|
||||
@@ -228,11 +226,7 @@ func (r *groupRepository) GetByIDLite(ctx context.Context, id int64) (*service.G
|
||||
if err != nil {
|
||||
return nil, translatePersistenceError(err, service.ErrGroupNotFound, nil)
|
||||
}
|
||||
out := groupEntityToService(m)
|
||||
if prices, loadErr := loadGroupVideoModelPrices(ctx, r.sql, []int64{id}); loadErr == nil {
|
||||
applyVideoModelPricesToGroup(out, prices)
|
||||
}
|
||||
return out, nil
|
||||
return groupEntityToService(m), nil
|
||||
}
|
||||
|
||||
func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) error {
|
||||
@@ -261,6 +255,7 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er
|
||||
SetNillableVideoPrice480p(groupIn.VideoPrice480P).
|
||||
SetNillableVideoPrice720p(groupIn.VideoPrice720P).
|
||||
SetNillableVideoPrice1080p(groupIn.VideoPrice1080P).
|
||||
SetVideoModelPrices(service.NormalizeVideoModelPrices(groupIn.VideoModelPrices)).
|
||||
SetDefaultValidityDays(groupIn.DefaultValidityDays).
|
||||
SetClaudeCodeOnly(groupIn.ClaudeCodeOnly).
|
||||
SetModelRoutingEnabled(groupIn.ModelRoutingEnabled).
|
||||
@@ -363,9 +358,6 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er
|
||||
return translatePersistenceError(err, service.ErrGroupNotFound, service.ErrGroupExists)
|
||||
}
|
||||
groupIn.UpdatedAt = updated.UpdatedAt
|
||||
if err := saveGroupVideoModelPrices(ctx, r.sql, groupIn.ID, groupIn.VideoModelPrices); err != nil {
|
||||
return fmt.Errorf("save group video_model_prices: %w", err)
|
||||
}
|
||||
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventGroupChanged, nil, &groupIn.ID, nil); err != nil {
|
||||
logger.LegacyPrintf("repository.group", "[SchedulerOutbox] enqueue group update failed: group=%d err=%v", groupIn.ID, err)
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func loadGroupVideoModelPrices(ctx context.Context, sqlq sqlExecutor, groupIDs []int64) (map[int64]map[string]map[string]float64, error) {
|
||||
out := make(map[int64]map[string]map[string]float64, len(groupIDs))
|
||||
if sqlq == nil || len(groupIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
rows, err := sqlq.QueryContext(ctx, `
|
||||
SELECT id, video_model_prices
|
||||
FROM groups
|
||||
WHERE id = ANY($1) AND deleted_at IS NULL
|
||||
`, pq.Array(groupIDs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
groupID int64
|
||||
raw []byte
|
||||
)
|
||||
if err := rows.Scan(&groupID, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prices, err := decodeVideoModelPrices(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prices != nil {
|
||||
out[groupID] = prices
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func saveGroupVideoModelPrices(ctx context.Context, sqlq sqlExecutor, groupID int64, prices map[string]map[string]float64) error {
|
||||
if sqlq == nil || groupID <= 0 {
|
||||
return nil
|
||||
}
|
||||
normalized := service.NormalizeVideoModelPrices(prices)
|
||||
if len(normalized) == 0 {
|
||||
_, err := sqlq.ExecContext(ctx, `
|
||||
UPDATE groups
|
||||
SET video_model_prices = NULL
|
||||
WHERE id = $1
|
||||
`, groupID)
|
||||
return err
|
||||
}
|
||||
payload, err := json.Marshal(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = sqlq.ExecContext(ctx, `
|
||||
UPDATE groups
|
||||
SET video_model_prices = $1::jsonb
|
||||
WHERE id = $2
|
||||
`, string(payload), groupID)
|
||||
return err
|
||||
}
|
||||
|
||||
func applyVideoModelPricesToGroups(groups []service.Group, pricesByID map[int64]map[string]map[string]float64) {
|
||||
for i := range groups {
|
||||
if prices, ok := pricesByID[groups[i].ID]; ok {
|
||||
groups[i].VideoModelPrices = service.NormalizeVideoModelPrices(prices)
|
||||
continue
|
||||
}
|
||||
groups[i].VideoModelPrices = service.NormalizeVideoModelPrices(groups[i].VideoModelPrices)
|
||||
}
|
||||
}
|
||||
|
||||
func applyVideoModelPricesToGroup(group *service.Group, pricesByID map[int64]map[string]map[string]float64) {
|
||||
if group == nil {
|
||||
return
|
||||
}
|
||||
if prices, ok := pricesByID[group.ID]; ok {
|
||||
group.VideoModelPrices = service.NormalizeVideoModelPrices(prices)
|
||||
return
|
||||
}
|
||||
group.VideoModelPrices = service.NormalizeVideoModelPrices(group.VideoModelPrices)
|
||||
}
|
||||
|
||||
func decodeVideoModelPrices(raw []byte) (map[string]map[string]float64, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Driver may return NULL as nil slice; treat empty JSON as nil.
|
||||
trimmed := string(raw)
|
||||
if trimmed == "" || trimmed == "null" {
|
||||
return nil, nil
|
||||
}
|
||||
var parsed map[string]map[string]float64
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
// Some drivers surface NULL via sql.NullString paths; tolerate empty object.
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return service.NormalizeVideoModelPrices(parsed), nil
|
||||
}
|
||||
@@ -67,6 +67,21 @@ func cloneGroupModelRouting(value map[string][]int64) map[string][]int64 {
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneGroupVideoModelPrices(value map[string]map[string]float64) map[string]map[string]float64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]map[string]float64, len(value))
|
||||
for model, prices := range value {
|
||||
clonedPrices := make(map[string]float64, len(prices))
|
||||
for resolution, price := range prices {
|
||||
clonedPrices[resolution] = price
|
||||
}
|
||||
cloned[model] = clonedPrices
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneGroupMessagesDispatchModelConfig(value OpenAIMessagesDispatchModelConfig) OpenAIMessagesDispatchModelConfig {
|
||||
cloned := value
|
||||
if value.ExactModelMappings != nil {
|
||||
@@ -113,6 +128,7 @@ func cloneGroupForDuplicate(source *Group, operationID string) *Group {
|
||||
VideoPrice480P: cloneGroupValuePointer(source.VideoPrice480P),
|
||||
VideoPrice720P: cloneGroupValuePointer(source.VideoPrice720P),
|
||||
VideoPrice1080P: cloneGroupValuePointer(source.VideoPrice1080P),
|
||||
VideoModelPrices: cloneGroupVideoModelPrices(source.VideoModelPrices),
|
||||
WebSearchPricePerCall: cloneGroupValuePointer(source.WebSearchPricePerCall),
|
||||
ClaudeCodeOnly: source.ClaudeCodeOnly,
|
||||
FallbackGroupID: cloneGroupValuePointer(source.FallbackGroupID),
|
||||
|
||||
@@ -121,37 +121,40 @@ func groupDuplicateTestPointer[T any](value T) *T { return &value }
|
||||
func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing.T) {
|
||||
createdAt := time.Date(2026, time.July, 1, 2, 3, 4, 0, time.UTC)
|
||||
source := &Group{
|
||||
ID: 41,
|
||||
Name: "高级订阅",
|
||||
Description: "configuration",
|
||||
Platform: PlatformOpenAI,
|
||||
RateMultiplier: 1.75,
|
||||
PeakRateEnabled: true,
|
||||
PeakStart: "09:00",
|
||||
PeakEnd: "18:00",
|
||||
PeakRateMultiplier: 1.2,
|
||||
IsExclusive: true,
|
||||
Status: StatusActive,
|
||||
Hydrated: true,
|
||||
SubscriptionType: SubscriptionTypeSubscription,
|
||||
DailyLimitUSD: groupDuplicateTestPointer(11.0),
|
||||
WeeklyLimitUSD: groupDuplicateTestPointer(22.0),
|
||||
MonthlyLimitUSD: groupDuplicateTestPointer(33.0),
|
||||
DefaultValidityDays: 91,
|
||||
AllowImageGeneration: true,
|
||||
AllowBatchImageGeneration: true,
|
||||
ImageRateIndependent: true,
|
||||
ImageRateMultiplier: 1.4,
|
||||
ImagePrice1K: groupDuplicateTestPointer(0.01),
|
||||
ImagePrice2K: groupDuplicateTestPointer(0.02),
|
||||
ImagePrice4K: groupDuplicateTestPointer(0.04),
|
||||
BatchImageDiscountMultiplier: 0.4,
|
||||
BatchImageHoldMultiplier: 0.7,
|
||||
VideoRateIndependent: true,
|
||||
VideoRateMultiplier: 2.1,
|
||||
VideoPrice480P: groupDuplicateTestPointer(0.1),
|
||||
VideoPrice720P: groupDuplicateTestPointer(0.2),
|
||||
VideoPrice1080P: groupDuplicateTestPointer(0.3),
|
||||
ID: 41,
|
||||
Name: "高级订阅",
|
||||
Description: "configuration",
|
||||
Platform: PlatformOpenAI,
|
||||
RateMultiplier: 1.75,
|
||||
PeakRateEnabled: true,
|
||||
PeakStart: "09:00",
|
||||
PeakEnd: "18:00",
|
||||
PeakRateMultiplier: 1.2,
|
||||
IsExclusive: true,
|
||||
Status: StatusActive,
|
||||
Hydrated: true,
|
||||
SubscriptionType: SubscriptionTypeSubscription,
|
||||
DailyLimitUSD: groupDuplicateTestPointer(11.0),
|
||||
WeeklyLimitUSD: groupDuplicateTestPointer(22.0),
|
||||
MonthlyLimitUSD: groupDuplicateTestPointer(33.0),
|
||||
DefaultValidityDays: 91,
|
||||
AllowImageGeneration: true,
|
||||
AllowBatchImageGeneration: true,
|
||||
ImageRateIndependent: true,
|
||||
ImageRateMultiplier: 1.4,
|
||||
ImagePrice1K: groupDuplicateTestPointer(0.01),
|
||||
ImagePrice2K: groupDuplicateTestPointer(0.02),
|
||||
ImagePrice4K: groupDuplicateTestPointer(0.04),
|
||||
BatchImageDiscountMultiplier: 0.4,
|
||||
BatchImageHoldMultiplier: 0.7,
|
||||
VideoRateIndependent: true,
|
||||
VideoRateMultiplier: 2.1,
|
||||
VideoPrice480P: groupDuplicateTestPointer(0.1),
|
||||
VideoPrice720P: groupDuplicateTestPointer(0.2),
|
||||
VideoPrice1080P: groupDuplicateTestPointer(0.3),
|
||||
VideoModelPrices: map[string]map[string]float64{
|
||||
VideoPriceFamilyGrokImagineVideo15: {VideoBillingResolution720P: 0.14},
|
||||
},
|
||||
WebSearchPricePerCall: groupDuplicateTestPointer(0.005),
|
||||
ClaudeCodeOnly: true,
|
||||
FallbackGroupID: groupDuplicateTestPointer(int64(7)),
|
||||
@@ -204,6 +207,7 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
|
||||
require.Equal(t, source.PeakRateMultiplier, duplicate.PeakRateMultiplier)
|
||||
require.Equal(t, source.DefaultValidityDays, duplicate.DefaultValidityDays)
|
||||
require.Equal(t, source.ImagePrice4K, duplicate.ImagePrice4K)
|
||||
require.Equal(t, source.VideoModelPrices, duplicate.VideoModelPrices)
|
||||
require.Equal(t, source.WebSearchPricePerCall, duplicate.WebSearchPricePerCall)
|
||||
require.Equal(t, source.FallbackGroupID, duplicate.FallbackGroupID)
|
||||
require.Equal(t, source.ModelRouting, duplicate.ModelRouting)
|
||||
@@ -222,12 +226,14 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
|
||||
}, repo.createdBindings[duplicate.ID])
|
||||
|
||||
duplicate.ModelRouting["gpt-*"][0] = 999
|
||||
duplicate.VideoModelPrices[VideoPriceFamilyGrokImagineVideo15][VideoBillingResolution720P] = 999
|
||||
duplicate.SupportedModelScopes[0] = "changed"
|
||||
duplicate.MessagesDispatchModelConfig.ExactModelMappings["claude-special"] = "changed"
|
||||
duplicate.ModelsListConfig.Models[0] = "changed"
|
||||
duplicate.ReasoningEffortMappings[0].To = "changed"
|
||||
*duplicate.DailyLimitUSD = 999
|
||||
require.Equal(t, int64(13), source.ModelRouting["gpt-*"][0])
|
||||
require.Equal(t, 0.14, source.VideoModelPrices[VideoPriceFamilyGrokImagineVideo15][VideoBillingResolution720P])
|
||||
require.Equal(t, "claude", source.SupportedModelScopes[0])
|
||||
require.Equal(t, "gpt-special", source.MessagesDispatchModelConfig.ExactModelMappings["claude-special"])
|
||||
require.Equal(t, "gpt-5.4", source.ModelsListConfig.Models[0])
|
||||
|
||||
Reference in New Issue
Block a user