feat: improve model catalog aggregation
This commit is contained in:
@@ -17,7 +17,7 @@ SELECT p.id::text, p.platform_key, p.name, p.provider,
|
||||
p.retry_policy, p.rate_limit_policy,
|
||||
COALESCE(p.dynamic_priority, p.priority) AS effective_priority,
|
||||
m.id::text, COALESCE(m.base_model_id::text, ''), COALESCE(b.canonical_model_key, ''),
|
||||
COALESCE(b.provider_model_name, ''), m.model_name, COALESCE(m.model_alias, ''),
|
||||
COALESCE(NULLIF(m.provider_model_name, ''), m.model_name), m.model_name, COALESCE(m.model_alias, ''),
|
||||
$2 AS requested_model_type, m.display_name, m.capabilities, m.capability_override,
|
||||
COALESCE(b.base_billing_config, '{}'::jsonb), m.billing_config, m.billing_config_override,
|
||||
m.pricing_mode, COALESCE(m.discount_factor, 0)::float8, COALESCE(m.pricing_rule_set_id::text, ''),
|
||||
@@ -32,17 +32,22 @@ LEFT JOIN model_catalog_providers cp ON cp.provider_key = p.provider OR cp.provi
|
||||
LEFT JOIN base_model_catalog b ON b.id = m.base_model_id
|
||||
LEFT JOIN model_runtime_policy_sets rp ON rp.id = COALESCE(m.runtime_policy_set_id, b.runtime_policy_set_id)
|
||||
LEFT JOIN runtime_client_states s
|
||||
ON s.client_id = p.platform_key || ':' || $2 || ':' || m.model_name
|
||||
ON s.client_id = p.platform_key || ':' || $2 || ':' || COALESCE(NULLIF(m.provider_model_name, ''), m.model_name)
|
||||
WHERE p.status = 'enabled'
|
||||
AND p.deleted_at IS NULL
|
||||
AND m.enabled = true
|
||||
AND m.model_type @> jsonb_build_array($2)
|
||||
AND (p.cooldown_until IS NULL OR p.cooldown_until <= now())
|
||||
AND (
|
||||
m.model_name = $1
|
||||
OR m.model_alias = $1
|
||||
OR b.canonical_model_key = $1
|
||||
OR b.provider_model_name = $1
|
||||
(COALESCE(m.model_alias, '') <> '' AND m.model_alias = $1)
|
||||
OR (
|
||||
COALESCE(m.model_alias, '') = ''
|
||||
AND (
|
||||
m.model_name = $1
|
||||
OR b.canonical_model_key = $1
|
||||
OR b.provider_model_name = $1
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY effective_priority ASC,
|
||||
COALESCE(s.limiter_ratio, 0) ASC,
|
||||
@@ -137,7 +142,8 @@ ORDER BY effective_priority ASC,
|
||||
item.RuntimeRateLimitPolicy = decodeObject(runtimeRateLimitPolicy)
|
||||
item.AutoDisablePolicy = decodeObject(autoDisablePolicy)
|
||||
item.DegradePolicy = decodeObject(degradePolicy)
|
||||
item.ClientID = fmt.Sprintf("%s:%s:%s", item.PlatformKey, item.ModelType, item.ModelName)
|
||||
upstreamModelName := firstNonEmpty(item.ProviderModelName, item.ModelName)
|
||||
item.ClientID = fmt.Sprintf("%s:%s:%s", item.PlatformKey, item.ModelType, upstreamModelName)
|
||||
item.QueueKey = item.ClientID
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package store
|
||||
|
||||
import "strings"
|
||||
|
||||
func FilterPlatformModelBillingConfigs(models []PlatformModel) []PlatformModel {
|
||||
filtered := make([]PlatformModel, len(models))
|
||||
for i, model := range models {
|
||||
filtered[i] = FilterPlatformModelBillingConfig(model)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func FilterPlatformModelBillingConfig(model PlatformModel) PlatformModel {
|
||||
model.BillingConfig = filterBillingConfigByModelTypes(model.BillingConfig, model.ModelType)
|
||||
model.BillingConfigOverride = filterBillingConfigByModelTypes(model.BillingConfigOverride, model.ModelType)
|
||||
return model
|
||||
}
|
||||
|
||||
func filterBillingConfigByModelTypes(config map[string]any, modelTypes []string) map[string]any {
|
||||
if len(config) == 0 {
|
||||
return nil
|
||||
}
|
||||
resources := billingResourcesForModelTypes(modelTypes)
|
||||
if len(resources) == 0 {
|
||||
return cloneBillingConfig(config)
|
||||
}
|
||||
|
||||
filtered := map[string]any{}
|
||||
for key, value := range config {
|
||||
if billingConfigKeyAllowed(key, resources) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if resources["image_edit"] && !hasAnyKey(filtered, "image_edit", "editBase") && !hasAnyKey(config, "image_edit", "editBase") {
|
||||
copyBillingConfigKey(filtered, config, "image")
|
||||
copyBillingConfigKey(filtered, config, "imageBase")
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func billingResourcesForModelTypes(modelTypes []string) map[string]bool {
|
||||
resources := map[string]bool{}
|
||||
for _, modelType := range modelTypes {
|
||||
switch normalizeBillingType(modelType) {
|
||||
case "chat", "text", "responses", "text_generate", "text_embedding", "embedding",
|
||||
"image_analysis", "video_understanding", "audio_understanding", "omni", "tools_call":
|
||||
resources["text"] = true
|
||||
case "image", "images.generations", "image_generate":
|
||||
resources["image"] = true
|
||||
case "images.edits", "image_edit":
|
||||
resources["image_edit"] = true
|
||||
case "video", "videos.generations", "video_generate", "image_to_video", "text_to_video",
|
||||
"video_edit", "omni_video", "video_reference", "video_first_last_frame":
|
||||
resources["video"] = true
|
||||
case "audio", "audio_generate", "text_to_speech", "speech":
|
||||
resources["audio"] = true
|
||||
case "music", "music_generate":
|
||||
resources["music"] = true
|
||||
case "digital_human", "digital_human_generate":
|
||||
resources["digital_human"] = true
|
||||
case "model", "model_3d", "text_to_model", "image_to_model", "multiview_to_model", "mesh_edit":
|
||||
resources["model"] = true
|
||||
default:
|
||||
inferBillingResources(modelType, resources)
|
||||
}
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func inferBillingResources(modelType string, resources map[string]bool) {
|
||||
normalized := normalizeBillingType(modelType)
|
||||
switch {
|
||||
case strings.Contains(normalized, "digital_human"):
|
||||
resources["digital_human"] = true
|
||||
case strings.Contains(normalized, "video"):
|
||||
resources["video"] = true
|
||||
case strings.Contains(normalized, "image"):
|
||||
resources["image"] = true
|
||||
case strings.Contains(normalized, "audio") || strings.Contains(normalized, "speech"):
|
||||
resources["audio"] = true
|
||||
case strings.Contains(normalized, "music"):
|
||||
resources["music"] = true
|
||||
case strings.Contains(normalized, "model") || strings.Contains(normalized, "mesh"):
|
||||
resources["model"] = true
|
||||
case strings.Contains(normalized, "text") || strings.Contains(normalized, "token"):
|
||||
resources["text"] = true
|
||||
}
|
||||
}
|
||||
|
||||
func billingConfigKeyAllowed(key string, resources map[string]bool) bool {
|
||||
switch normalizeBillingConfigKey(key) {
|
||||
case "text", "texttotal", "text_total", "textinputper1k", "textoutputper1k", "textinput", "textoutput", "text_input", "text_output", "inputtokenprice", "outputtokenprice":
|
||||
return resources["text"]
|
||||
case "image", "imagebase":
|
||||
return resources["image"]
|
||||
case "image_edit", "imageedit", "editbase":
|
||||
return resources["image_edit"]
|
||||
case "video", "videobase":
|
||||
return resources["video"]
|
||||
case "audio", "audiobase":
|
||||
return resources["audio"]
|
||||
case "music", "musicbase":
|
||||
return resources["music"]
|
||||
case "digital_human", "digitalhuman", "digitalhumanbase":
|
||||
return resources["digital_human"]
|
||||
case "model", "modelbase":
|
||||
return resources["model"]
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBillingType(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func normalizeBillingConfigKey(value string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(normalizeBillingType(value), "-", "_"), " ", "")
|
||||
}
|
||||
|
||||
func cloneBillingConfig(config map[string]any) map[string]any {
|
||||
clone := make(map[string]any, len(config))
|
||||
for key, value := range config {
|
||||
clone[key] = value
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func hasAnyKey(config map[string]any, keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
if _, ok := config[key]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func copyBillingConfigKey(target map[string]any, source map[string]any, key string) {
|
||||
if value, ok := source[key]; ok {
|
||||
target[key] = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFilterPlatformModelBillingConfigKeepsOnlyImagePricing(t *testing.T) {
|
||||
model := PlatformModel{
|
||||
ModelType: StringList{"image_generate"},
|
||||
BillingConfig: map[string]any{
|
||||
"text": map[string]any{"basePrice": 0.01},
|
||||
"image": map[string]any{"basePrice": 10},
|
||||
"image_edit": map[string]any{"basePrice": 12},
|
||||
"video": map[string]any{"basePrice": 100},
|
||||
},
|
||||
BillingConfigOverride: map[string]any{
|
||||
"image": map[string]any{"basePrice": 8},
|
||||
"video": map[string]any{"basePrice": 80},
|
||||
},
|
||||
}
|
||||
|
||||
filtered := FilterPlatformModelBillingConfig(model)
|
||||
|
||||
assertHasKeys(t, filtered.BillingConfig, "image")
|
||||
assertMissingKeys(t, filtered.BillingConfig, "text", "image_edit", "video")
|
||||
assertHasKeys(t, filtered.BillingConfigOverride, "image")
|
||||
assertMissingKeys(t, filtered.BillingConfigOverride, "video")
|
||||
}
|
||||
|
||||
func TestFilterPlatformModelBillingConfigKeepsImageEditOrImageFallback(t *testing.T) {
|
||||
model := PlatformModel{
|
||||
ModelType: StringList{"image_edit"},
|
||||
BillingConfig: map[string]any{
|
||||
"image": map[string]any{"basePrice": 10},
|
||||
"video": map[string]any{"basePrice": 100},
|
||||
},
|
||||
}
|
||||
|
||||
filtered := FilterPlatformModelBillingConfig(model)
|
||||
|
||||
assertHasKeys(t, filtered.BillingConfig, "image")
|
||||
assertMissingKeys(t, filtered.BillingConfig, "video")
|
||||
}
|
||||
|
||||
func TestFilterPlatformModelBillingConfigKeepsVideoPricing(t *testing.T) {
|
||||
model := PlatformModel{
|
||||
ModelType: StringList{"video_generate", "image_to_video"},
|
||||
BillingConfig: map[string]any{
|
||||
"image": map[string]any{"basePrice": 10},
|
||||
"video": map[string]any{"basePrice": 100},
|
||||
"videoBase": 100,
|
||||
},
|
||||
}
|
||||
|
||||
filtered := FilterPlatformModelBillingConfig(model)
|
||||
|
||||
assertHasKeys(t, filtered.BillingConfig, "video", "videoBase")
|
||||
assertMissingKeys(t, filtered.BillingConfig, "image")
|
||||
}
|
||||
|
||||
func TestFilterPlatformModelBillingConfigKeepsTextFlatPricing(t *testing.T) {
|
||||
model := PlatformModel{
|
||||
ModelType: StringList{"text_generate"},
|
||||
BillingConfig: map[string]any{
|
||||
"textInputPer1k": 0.01,
|
||||
"textOutputPer1k": 0.02,
|
||||
"text_total": map[string]any{
|
||||
"basePrice": 0.01,
|
||||
"formulaConfig": map[string]any{"inputTokenPrice": 0.01, "outputTokenPrice": 0.02},
|
||||
"dynamicWeight": map[string]any{"cached": 0.5},
|
||||
"dimensionSchema": map[string]any{"unit": "1k_tokens"},
|
||||
},
|
||||
"inputTokenPrice": 0.01,
|
||||
"outputTokenPrice": 0.02,
|
||||
"image": map[string]any{"basePrice": 10},
|
||||
},
|
||||
}
|
||||
|
||||
filtered := FilterPlatformModelBillingConfig(model)
|
||||
|
||||
assertHasKeys(t, filtered.BillingConfig, "textInputPer1k", "textOutputPer1k", "text_total", "inputTokenPrice", "outputTokenPrice")
|
||||
assertMissingKeys(t, filtered.BillingConfig, "image")
|
||||
}
|
||||
|
||||
func assertHasKeys(t *testing.T, value map[string]any, keys ...string) {
|
||||
t.Helper()
|
||||
for _, key := range keys {
|
||||
if _, ok := value[key]; !ok {
|
||||
t.Fatalf("expected key %q in %#v", key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertMissingKeys(t *testing.T, value map[string]any, keys ...string) {
|
||||
t.Helper()
|
||||
for _, key := range keys {
|
||||
if _, ok := value[key]; ok {
|
||||
t.Fatalf("expected key %q to be filtered from %#v", key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,8 @@ WHERE resource_type = 'platform_model'
|
||||
}
|
||||
|
||||
func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier, input CreatePlatformModelInput) (PlatformModel, error) {
|
||||
input.ModelName = strings.TrimSpace(input.ModelName)
|
||||
input.ProviderModelName = strings.TrimSpace(input.ProviderModelName)
|
||||
base, err := s.lookupBaseModel(ctx, q, input.BaseModelID, input.CanonicalModelKey, input.ModelName)
|
||||
if err != nil && !IsNotFound(err) {
|
||||
return PlatformModel{}, err
|
||||
@@ -93,6 +95,9 @@ func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier,
|
||||
if input.ModelName == "" {
|
||||
input.ModelName = base.ProviderModelName
|
||||
}
|
||||
if input.ProviderModelName == "" {
|
||||
input.ProviderModelName = input.ModelName
|
||||
}
|
||||
if input.DisplayName == "" {
|
||||
input.DisplayName = firstNonEmpty(base.DisplayName, input.ModelName)
|
||||
}
|
||||
@@ -153,19 +158,20 @@ func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier,
|
||||
var modelTypeBytes []byte
|
||||
err = q.QueryRow(ctx, `
|
||||
INSERT INTO platform_models (
|
||||
platform_id, base_model_id, model_name, model_alias, model_type, display_name,
|
||||
platform_id, base_model_id, model_name, provider_model_name, model_alias, model_type, display_name,
|
||||
capability_override, capabilities, pricing_mode, discount_factor,
|
||||
pricing_rule_set_id, billing_config_override, billing_config, permission_config, retry_policy, rate_limit_policy,
|
||||
runtime_policy_set_id, runtime_policy_override, enabled
|
||||
)
|
||||
VALUES (
|
||||
$1::uuid, $2::uuid, $3, NULLIF($4, ''), $5::jsonb, $6,
|
||||
$7::jsonb, $8::jsonb, $9, $10::numeric,
|
||||
NULLIF($11, '')::uuid, $12::jsonb, $13::jsonb, $14::jsonb, $15::jsonb, $16::jsonb,
|
||||
NULLIF($17, '')::uuid, $18::jsonb, true
|
||||
$1::uuid, $2::uuid, $3, NULLIF($4, ''), NULLIF($5, ''), $6::jsonb, $7,
|
||||
$8::jsonb, $9::jsonb, $10, $11::numeric,
|
||||
NULLIF($12, '')::uuid, $13::jsonb, $14::jsonb, $15::jsonb, $16::jsonb, $17::jsonb,
|
||||
NULLIF($18, '')::uuid, $19::jsonb, true
|
||||
)
|
||||
ON CONFLICT (platform_id, model_name) DO UPDATE
|
||||
SET base_model_id = EXCLUDED.base_model_id,
|
||||
provider_model_name = EXCLUDED.provider_model_name,
|
||||
model_alias = EXCLUDED.model_alias,
|
||||
display_name = EXCLUDED.display_name,
|
||||
capability_override = EXCLUDED.capability_override,
|
||||
@@ -183,7 +189,7 @@ SET base_model_id = EXCLUDED.base_model_id,
|
||||
enabled = true,
|
||||
updated_at = now()
|
||||
RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_name,
|
||||
COALESCE(model_alias, ''), model_type, display_name, capability_override,
|
||||
COALESCE(NULLIF(provider_model_name, ''), model_name), COALESCE(model_alias, ''), model_type, display_name, capability_override,
|
||||
capabilities, pricing_mode, COALESCE(discount_factor, 0)::float8,
|
||||
COALESCE(pricing_rule_set_id::text, ''), billing_config_override, billing_config,
|
||||
permission_config, retry_policy, rate_limit_policy, COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override,
|
||||
@@ -191,6 +197,7 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
input.PlatformID,
|
||||
baseID,
|
||||
input.ModelName,
|
||||
input.ProviderModelName,
|
||||
input.ModelAlias,
|
||||
string(modelTypeJSON),
|
||||
input.DisplayName,
|
||||
@@ -211,6 +218,7 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
&model.PlatformID,
|
||||
&model.BaseModelID,
|
||||
&model.ModelName,
|
||||
&model.ProviderModelName,
|
||||
&model.ModelAlias,
|
||||
&modelTypeBytes,
|
||||
&model.DisplayName,
|
||||
|
||||
@@ -133,6 +133,7 @@ type PlatformModel struct {
|
||||
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"`
|
||||
@@ -691,7 +692,7 @@ 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(m.model_alias, ''), m.model_type, m.display_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, 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,
|
||||
@@ -724,6 +725,7 @@ ORDER BY m.model_type ASC, m.model_name ASC`, args...)
|
||||
&model.Provider,
|
||||
&model.PlatformName,
|
||||
&model.ModelName,
|
||||
&model.ProviderModelName,
|
||||
&model.ModelAlias,
|
||||
&modelTypeBytes,
|
||||
&model.DisplayName,
|
||||
|
||||
@@ -3,6 +3,7 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -182,7 +183,7 @@ func (s *Store) PricingRuleSetBillingConfig(ctx context.Context, id string) (map
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT resource_type, base_price::float8, dynamic_weight
|
||||
SELECT resource_type, base_price::float8, dynamic_weight, formula_config
|
||||
FROM model_pricing_rules
|
||||
WHERE rule_set_id = $1::uuid
|
||||
AND status = 'active'
|
||||
@@ -197,15 +198,31 @@ ORDER BY priority ASC, resource_type ASC`, id)
|
||||
var resourceType string
|
||||
var basePrice float64
|
||||
var dynamicWeightBytes []byte
|
||||
if err := rows.Scan(&resourceType, &basePrice, &dynamicWeightBytes); err != nil {
|
||||
var formulaConfigBytes []byte
|
||||
if err := rows.Scan(&resourceType, &basePrice, &dynamicWeightBytes, &formulaConfigBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dynamicWeight := decodeObject(dynamicWeightBytes)
|
||||
formulaConfig := decodeObject(formulaConfigBytes)
|
||||
switch resourceType {
|
||||
case "text_input":
|
||||
config["textInputPer1k"] = basePrice
|
||||
case "text_output":
|
||||
config["textOutputPer1k"] = basePrice
|
||||
case "text_total":
|
||||
inputPrice := basePrice
|
||||
if value, ok := pricingRuleNumberFromKeys(formulaConfig, "inputTokenPrice", "input_token_price", "textInputPer1k", "text_input"); ok {
|
||||
inputPrice = value
|
||||
}
|
||||
config["textInputPer1k"] = inputPrice
|
||||
if outputPrice, ok := pricingRuleNumberFromKeys(formulaConfig, "outputTokenPrice", "output_token_price", "textOutputPer1k", "text_output"); ok {
|
||||
config["textOutputPer1k"] = outputPrice
|
||||
}
|
||||
resourceConfig := pricingResourceConfig(basePrice, dynamicWeight)
|
||||
if len(formulaConfig) > 0 {
|
||||
resourceConfig["formulaConfig"] = formulaConfig
|
||||
}
|
||||
config["text_total"] = resourceConfig
|
||||
case "image":
|
||||
config["imageBase"] = basePrice
|
||||
config["image"] = pricingResourceConfig(basePrice, dynamicWeight)
|
||||
@@ -225,6 +242,45 @@ ORDER BY priority ASC, resource_type ASC`, id)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func pricingRuleNumberFromKeys(config map[string]any, keys ...string) (float64, bool) {
|
||||
if len(config) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
for _, key := range keys {
|
||||
if value, ok := pricingRuleNumberValue(config[key]); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func pricingRuleNumberValue(value any) (float64, bool) {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
return typed, true
|
||||
case float32:
|
||||
return float64(typed), true
|
||||
case int:
|
||||
return float64(typed), true
|
||||
case int64:
|
||||
return float64(typed), true
|
||||
case int32:
|
||||
return float64(typed), true
|
||||
case json.Number:
|
||||
number, err := typed.Float64()
|
||||
return number, err == nil
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(typed)
|
||||
if trimmed == "" {
|
||||
return 0, false
|
||||
}
|
||||
number, err := strconv.ParseFloat(trimmed, 64)
|
||||
return number, err == nil
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func pricingResourceConfig(basePrice float64, dynamicWeight map[string]any) map[string]any {
|
||||
config := map[string]any{"basePrice": basePrice}
|
||||
if len(dynamicWeight) > 0 {
|
||||
|
||||
@@ -15,6 +15,7 @@ type CreatePlatformModelInput struct {
|
||||
BaseModelID string `json:"baseModelId"`
|
||||
CanonicalModelKey string `json:"canonicalModelKey"`
|
||||
ModelName string `json:"modelName"`
|
||||
ProviderModelName string `json:"providerModelName"`
|
||||
ModelAlias string `json:"modelAlias"`
|
||||
ModelType StringList `json:"modelType"`
|
||||
DisplayName string `json:"displayName"`
|
||||
|
||||
@@ -11,13 +11,35 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
func (s *Store) ListTasks(ctx context.Context, user *auth.User, limit int) ([]GatewayTask, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
type TaskListFilter struct {
|
||||
Query string
|
||||
ModelType string
|
||||
CreatedFrom *time.Time
|
||||
CreatedTo *time.Time
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type TaskListResult struct {
|
||||
Items []GatewayTask
|
||||
Total int
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func (s *Store) ListTasks(ctx context.Context, user *auth.User, filter TaskListFilter) (TaskListResult, error) {
|
||||
page := filter.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
pageSize := filter.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 50
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
gatewayUserID := localGatewayUserID(user)
|
||||
apiKeyID := ""
|
||||
userID := ""
|
||||
@@ -26,11 +48,22 @@ func (s *Store) ListTasks(ctx context.Context, user *auth.User, limit int) ([]Ga
|
||||
userID = strings.TrimSpace(user.ID)
|
||||
}
|
||||
if gatewayUserID == "" && userID == "" {
|
||||
return nil, ErrLocalUserRequired
|
||||
return TaskListResult{}, ErrLocalUserRequired
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+gatewayTaskColumns+`
|
||||
FROM gateway_tasks
|
||||
queryPattern := ""
|
||||
if query := strings.TrimSpace(filter.Query); query != "" {
|
||||
queryPattern = "%" + query + "%"
|
||||
}
|
||||
args := []any{
|
||||
gatewayUserID,
|
||||
userID,
|
||||
apiKeyID,
|
||||
queryPattern,
|
||||
strings.TrimSpace(filter.ModelType),
|
||||
nullableTaskListTime(filter.CreatedFrom),
|
||||
nullableTaskListTime(filter.CreatedTo),
|
||||
}
|
||||
whereSQL := `
|
||||
WHERE (
|
||||
(
|
||||
NULLIF($1, '')::uuid IS NOT NULL
|
||||
@@ -46,10 +79,45 @@ WHERE (
|
||||
NULLIF($3, '') IS NULL
|
||||
OR api_key_id = $3
|
||||
)
|
||||
AND (
|
||||
NULLIF($4, '') IS NULL
|
||||
OR id::text ILIKE $4
|
||||
OR COALESCE(request_id, '') ILIKE $4
|
||||
OR kind ILIKE $4
|
||||
OR model ILIKE $4
|
||||
OR COALESCE(requested_model, '') ILIKE $4
|
||||
OR COALESCE(resolved_model, '') ILIKE $4
|
||||
OR COALESCE(api_key_id, '') ILIKE $4
|
||||
OR COALESCE(api_key_name, '') ILIKE $4
|
||||
OR COALESCE(api_key_prefix, '') ILIKE $4
|
||||
OR status ILIKE $4
|
||||
OR COALESCE(model_type, '') ILIKE $4
|
||||
)
|
||||
AND (
|
||||
NULLIF($5, '') IS NULL
|
||||
OR model_type = $5
|
||||
)
|
||||
AND (
|
||||
$6::timestamptz IS NULL
|
||||
OR created_at >= $6::timestamptz
|
||||
)
|
||||
AND (
|
||||
$7::timestamptz IS NULL
|
||||
OR created_at <= $7::timestamptz
|
||||
)`
|
||||
var total int
|
||||
if err := s.pool.QueryRow(ctx, `SELECT count(*) FROM gateway_tasks `+whereSQL, args...).Scan(&total); err != nil {
|
||||
return TaskListResult{}, err
|
||||
}
|
||||
queryArgs := append(args, pageSize, offset)
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+gatewayTaskColumns+`
|
||||
FROM gateway_tasks
|
||||
`+whereSQL+`
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $4`, gatewayUserID, userID, apiKeyID, limit)
|
||||
LIMIT $8 OFFSET $9`, queryArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return TaskListResult{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -57,11 +125,26 @@ LIMIT $4`, gatewayUserID, userID, apiKeyID, limit)
|
||||
for rows.Next() {
|
||||
task, err := scanGatewayTask(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return TaskListResult{}, err
|
||||
}
|
||||
items = append(items, task)
|
||||
}
|
||||
return items, rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
return TaskListResult{}, err
|
||||
}
|
||||
return TaskListResult{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func nullableTaskListTime(value *time.Time) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func (s *Store) MarkTaskRunning(ctx context.Context, taskID string, modelType string, normalizedRequest map[string]any) error {
|
||||
|
||||
Reference in New Issue
Block a user