Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
1d3a4f1da9
@ -7,6 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) platformModelResponse(ctx context.Context, model store.PlatformModel) store.PlatformModel {
|
||||
model.Capabilities = store.EffectivePlatformModelCapabilities(model.BaseCapabilities, model.Capabilities)
|
||||
model.Capabilities = enrichResponseCapabilities(model)
|
||||
model = s.withEffectiveResponseBillingConfig(ctx, model)
|
||||
return store.FilterPlatformModelBillingConfig(model)
|
||||
}
|
||||
@ -46,3 +48,167 @@ func mergeResponseBillingConfig(base map[string]any, override map[string]any) ma
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func enrichResponseCapabilities(model store.PlatformModel) map[string]any {
|
||||
if len(model.Capabilities) == 0 {
|
||||
return model.Capabilities
|
||||
}
|
||||
textGenerate, ok := enrichedTextGenerateCapabilities(model)
|
||||
if !ok {
|
||||
return model.Capabilities
|
||||
}
|
||||
|
||||
out := make(map[string]any, len(model.Capabilities)+1)
|
||||
for key, value := range model.Capabilities {
|
||||
out[key] = value
|
||||
}
|
||||
out["text_generate"] = textGenerate
|
||||
return out
|
||||
}
|
||||
|
||||
func enrichedTextGenerateCapabilities(model store.PlatformModel) (map[string]any, bool) {
|
||||
textGenerate := nestedCapabilities(model.Capabilities, "text_generate")
|
||||
if textGenerate == nil && !declaresModelType(model, "text_generate") {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
patch := map[string]any{}
|
||||
if _, ok := textGenerate["max_context_tokens"]; !ok {
|
||||
if value, ok := textGenerateContextTokens(model, textGenerate); ok {
|
||||
patch["max_context_tokens"] = value
|
||||
}
|
||||
}
|
||||
if _, ok := textGenerate["supportThinking"]; !ok {
|
||||
if value, ok := textGenerateSupportThinking(model, textGenerate); ok {
|
||||
patch["supportThinking"] = value
|
||||
}
|
||||
}
|
||||
if _, ok := textGenerate["thinkingEffortLevels"]; !ok {
|
||||
if value, ok := textGenerateThinkingEffortLevels(model, textGenerate); ok {
|
||||
patch["thinkingEffortLevels"] = value
|
||||
}
|
||||
}
|
||||
if len(patch) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
out := make(map[string]any, len(textGenerate)+len(patch))
|
||||
for key, value := range textGenerate {
|
||||
out[key] = value
|
||||
}
|
||||
for key, value := range patch {
|
||||
out[key] = value
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func textGenerateContextTokens(model store.PlatformModel, textGenerate map[string]any) (any, bool) {
|
||||
if value, ok := capabilityValue(textGenerate, "maxContextTokens"); ok {
|
||||
return value, true
|
||||
}
|
||||
return capabilityValue(model.Capabilities, "max_context_tokens", "maxContextTokens", "contextWindow")
|
||||
}
|
||||
|
||||
func textGenerateSupportThinking(model store.PlatformModel, textGenerate map[string]any) (any, bool) {
|
||||
if value, ok := capabilityValue(model.Capabilities, "supportThinking"); ok {
|
||||
return value, true
|
||||
}
|
||||
if _, ok := capabilityValue(textGenerate, "thinkingEffortLevels"); ok {
|
||||
return true, true
|
||||
}
|
||||
if _, ok := capabilityValue(model.Capabilities, "thinkingEffortLevels"); ok {
|
||||
return true, true
|
||||
}
|
||||
return capabilityValue(model.Capabilities, "reasoning")
|
||||
}
|
||||
|
||||
func textGenerateThinkingEffortLevels(model store.PlatformModel, textGenerate map[string]any) (any, bool) {
|
||||
if value, ok := capabilityValue(model.Capabilities, "thinkingEffortLevels"); ok {
|
||||
return value, true
|
||||
}
|
||||
if hasTextGenerateThinkingCapability(model, textGenerate) {
|
||||
return []string{}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func hasTextGenerateThinkingCapability(model store.PlatformModel, textGenerate map[string]any) bool {
|
||||
if boolCapabilityValue(textGenerate, "supportThinking", "supportThinkingModeSwitch") {
|
||||
return true
|
||||
}
|
||||
if boolCapabilityValue(model.Capabilities, "supportThinking", "supportThinkingModeSwitch", "reasoning") {
|
||||
return true
|
||||
}
|
||||
_, ok := capabilityValue(textGenerate, "max_thinking_tokens", "maxThinkingTokens")
|
||||
return ok
|
||||
}
|
||||
|
||||
func declaresModelType(model store.PlatformModel, modelType string) bool {
|
||||
if containsString(model.ModelType, modelType) {
|
||||
return true
|
||||
}
|
||||
if originalTypes, ok := stringListValue(model.Capabilities["originalTypes"]); ok {
|
||||
return containsString(originalTypes, modelType)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsString(items []string, want string) bool {
|
||||
for _, item := range items {
|
||||
if item == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nestedCapabilities(capabilities map[string]any, key string) map[string]any {
|
||||
if nested, ok := capabilities[key].(map[string]any); ok {
|
||||
return nested
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func capabilityValue(capabilities map[string]any, keys ...string) (any, bool) {
|
||||
if len(capabilities) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
for _, key := range keys {
|
||||
if value, ok := capabilities[key]; ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func boolCapabilityValue(capabilities map[string]any, keys ...string) bool {
|
||||
if len(capabilities) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, key := range keys {
|
||||
flag, ok := capabilities[key].(bool)
|
||||
if ok && flag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stringListValue(value any) ([]string, bool) {
|
||||
switch items := value.(type) {
|
||||
case []string:
|
||||
return items, len(items) > 0
|
||||
case []any:
|
||||
out := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
text, ok := item.(string)
|
||||
if !ok || text == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, text)
|
||||
}
|
||||
return out, len(out) > 0
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
206
apps/api/internal/httpapi/model_response_test.go
Normal file
206
apps/api/internal/httpapi/model_response_test.go
Normal file
@ -0,0 +1,206 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestPlatformModelResponseExposesTextGenerateContextAndThinkingFromNestedCapabilities(t *testing.T) {
|
||||
model := store.PlatformModel{
|
||||
ModelName: "gemini-3-pro-preview",
|
||||
ModelType: store.StringList{"text_generate"},
|
||||
Capabilities: map[string]any{
|
||||
"text_generate": map[string]any{
|
||||
"max_context_tokens": 1000000,
|
||||
"supportThinking": true,
|
||||
"thinkingEffortLevels": []any{"minimal", "low", "medium", "high"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := (&Server{}).platformModelResponse(context.Background(), model)
|
||||
textGenerate := textGenerateCapabilities(t, response)
|
||||
|
||||
if textGenerate["max_context_tokens"] != 1000000 {
|
||||
t.Fatalf("expected text_generate.max_context_tokens 1000000, got %#v", textGenerate["max_context_tokens"])
|
||||
}
|
||||
if textGenerate["supportThinking"] != true {
|
||||
t.Fatalf("expected text_generate.supportThinking true, got %#v", textGenerate["supportThinking"])
|
||||
}
|
||||
assertStringListValue(t, textGenerate["thinkingEffortLevels"], []string{"minimal", "low", "medium", "high"})
|
||||
if _, ok := response.Capabilities["contextWindow"]; ok {
|
||||
t.Fatalf("expected contextWindow root alias to be omitted, got %#v", response.Capabilities["contextWindow"])
|
||||
}
|
||||
if _, ok := response.Capabilities["thinkingEffortLevels"]; ok {
|
||||
t.Fatalf("expected thinkingEffortLevels root alias to be omitted, got %#v", response.Capabilities["thinkingEffortLevels"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformModelResponseCopiesRootTextCapabilityFieldsToTextGenerate(t *testing.T) {
|
||||
model := store.PlatformModel{
|
||||
ModelName: "legacy-root-capability-model",
|
||||
ModelType: store.StringList{"text_generate"},
|
||||
Capabilities: map[string]any{
|
||||
"maxContextTokens": 128000,
|
||||
"supportThinking": true,
|
||||
"thinkingEffortLevels": []any{"low", "medium"},
|
||||
},
|
||||
}
|
||||
|
||||
response := (&Server{}).platformModelResponse(context.Background(), model)
|
||||
textGenerate := textGenerateCapabilities(t, response)
|
||||
|
||||
if textGenerate["max_context_tokens"] != 128000 {
|
||||
t.Fatalf("expected text_generate.max_context_tokens 128000, got %#v", textGenerate["max_context_tokens"])
|
||||
}
|
||||
if textGenerate["supportThinking"] != true {
|
||||
t.Fatalf("expected text_generate.supportThinking true, got %#v", textGenerate["supportThinking"])
|
||||
}
|
||||
assertStringListValue(t, textGenerate["thinkingEffortLevels"], []string{"low", "medium"})
|
||||
}
|
||||
|
||||
func TestPlatformModelResponseExposesEmptyThinkingEffortLevelsWhenOnlyThinkingSwitchIsConfigured(t *testing.T) {
|
||||
model := store.PlatformModel{
|
||||
ModelName: "thinking-switch-model",
|
||||
ModelType: store.StringList{"text_generate"},
|
||||
Capabilities: map[string]any{
|
||||
"text_generate": map[string]any{
|
||||
"max_context_tokens": 262144,
|
||||
"max_thinking_tokens": 32768,
|
||||
"supportThinking": true,
|
||||
"supportThinkingModeSwitch": true,
|
||||
"supportStructuredOutput": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := (&Server{}).platformModelResponse(context.Background(), model)
|
||||
textGenerate := textGenerateCapabilities(t, response)
|
||||
|
||||
if textGenerate["supportThinking"] != true {
|
||||
t.Fatalf("expected text_generate.supportThinking true, got %#v", textGenerate["supportThinking"])
|
||||
}
|
||||
assertStringListValue(t, textGenerate["thinkingEffortLevels"], []string{})
|
||||
}
|
||||
|
||||
func TestPlatformModelResponseInheritsMissingTextGenerateThinkingLevelsFromBaseModel(t *testing.T) {
|
||||
model := store.PlatformModel{
|
||||
ModelName: "glm-4-7-251222",
|
||||
ModelType: store.StringList{"text_generate"},
|
||||
BaseCapabilities: map[string]any{
|
||||
"originalTypes": []any{"text_generate"},
|
||||
"text_generate": map[string]any{
|
||||
"max_context_tokens": 204800,
|
||||
"supportThinking": true,
|
||||
"thinkingEffortLevels": []any{"none", "minimal", "low", "medium", "high"},
|
||||
},
|
||||
},
|
||||
Capabilities: map[string]any{
|
||||
"originalTypes": []any{"text_generate"},
|
||||
"text_generate": map[string]any{
|
||||
"max_context_tokens": 204800,
|
||||
"max_thinking_tokens": 131072,
|
||||
"supportThinking": true,
|
||||
"supportThinkingModeSwitch": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := (&Server{}).platformModelResponse(context.Background(), model)
|
||||
textGenerate := textGenerateCapabilities(t, response)
|
||||
|
||||
assertStringListValue(t, textGenerate["thinkingEffortLevels"], []string{"none", "minimal", "low", "medium", "high"})
|
||||
if textGenerate["max_thinking_tokens"] != 131072 {
|
||||
t.Fatalf("expected platform text_generate.max_thinking_tokens to be preserved, got %#v", textGenerate["max_thinking_tokens"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformModelResponseUsesOriginalTypesWhenModelTypeIsMissing(t *testing.T) {
|
||||
model := store.PlatformModel{
|
||||
ModelName: "catalog-snapshot-model",
|
||||
Capabilities: map[string]any{
|
||||
"originalTypes": []any{"text_generate"},
|
||||
"text_generate": map[string]any{
|
||||
"max_context_tokens": 262144,
|
||||
"supportThinking": true,
|
||||
"thinkingEffortLevels": []any{"high", "max"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := (&Server{}).platformModelResponse(context.Background(), model)
|
||||
textGenerate := textGenerateCapabilities(t, response)
|
||||
|
||||
if textGenerate["max_context_tokens"] != 262144 {
|
||||
t.Fatalf("expected text_generate.max_context_tokens 262144, got %#v", textGenerate["max_context_tokens"])
|
||||
}
|
||||
if textGenerate["supportThinking"] != true {
|
||||
t.Fatalf("expected text_generate.supportThinking true, got %#v", textGenerate["supportThinking"])
|
||||
}
|
||||
assertStringListValue(t, textGenerate["thinkingEffortLevels"], []string{"high", "max"})
|
||||
}
|
||||
|
||||
func TestPlatformModelResponsePreservesTextGenerateFieldsOverFallbacks(t *testing.T) {
|
||||
model := store.PlatformModel{
|
||||
ModelName: "reasoning-model-with-tools",
|
||||
ModelType: store.StringList{"text_generate", "tools_call"},
|
||||
Capabilities: map[string]any{
|
||||
"maxContextTokens": 999999,
|
||||
"supportThinking": false,
|
||||
"text_generate": map[string]any{
|
||||
"max_context_tokens": 1000000,
|
||||
"supportThinking": true,
|
||||
"thinkingEffortLevels": []any{"minimal", "low", "medium"},
|
||||
},
|
||||
"tools_call": map[string]any{
|
||||
"thinkingEffortLevels": []any{"medium", "high"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := (&Server{}).platformModelResponse(context.Background(), model)
|
||||
textGenerate := textGenerateCapabilities(t, response)
|
||||
|
||||
if textGenerate["max_context_tokens"] != 1000000 {
|
||||
t.Fatalf("expected text_generate.max_context_tokens to stay 1000000, got %#v", textGenerate["max_context_tokens"])
|
||||
}
|
||||
if textGenerate["supportThinking"] != true {
|
||||
t.Fatalf("expected text_generate.supportThinking to stay true, got %#v", textGenerate["supportThinking"])
|
||||
}
|
||||
assertStringListValue(t, textGenerate["thinkingEffortLevels"], []string{"minimal", "low", "medium"})
|
||||
}
|
||||
|
||||
func textGenerateCapabilities(t *testing.T, model store.PlatformModel) map[string]any {
|
||||
t.Helper()
|
||||
capabilities, ok := model.Capabilities["text_generate"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected capabilities.text_generate object, got %#v", model.Capabilities["text_generate"])
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func assertStringListValue(t *testing.T, got any, want []string) {
|
||||
t.Helper()
|
||||
var items []string
|
||||
switch value := got.(type) {
|
||||
case []string:
|
||||
items = value
|
||||
case []any:
|
||||
items = make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
text, ok := item.(string)
|
||||
if !ok {
|
||||
t.Fatalf("expected string list %v, got non-string item %#v in %#v", want, item, got)
|
||||
}
|
||||
items = append(items, text)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("expected string list %v, got %#v", want, got)
|
||||
}
|
||||
if !slices.Equal(items, want) {
|
||||
t.Fatalf("expected string list %v, got %v", want, items)
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,13 @@ func (s *Store) CreatePlatformModel(ctx context.Context, input CreatePlatformMod
|
||||
return s.createPlatformModel(ctx, s.pool, input)
|
||||
}
|
||||
|
||||
// EffectivePlatformModelCapabilities merges base defaults with platform overrides.
|
||||
// Nested capability objects are merged recursively so platform fields override
|
||||
// only the same nested keys while preserving other base capability defaults.
|
||||
func EffectivePlatformModelCapabilities(base map[string]any, override map[string]any) map[string]any {
|
||||
return mergeCapabilityObjects(base, override)
|
||||
}
|
||||
|
||||
func (s *Store) ReplacePlatformModels(ctx context.Context, platformID string, inputs []CreatePlatformModelInput) ([]PlatformModel, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@ -107,7 +114,7 @@ func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier,
|
||||
}
|
||||
capabilities := input.Capabilities
|
||||
if len(capabilities) == 0 {
|
||||
capabilities = mergeObjects(base.Capabilities, input.CapabilityOverride)
|
||||
capabilities = EffectivePlatformModelCapabilities(base.Capabilities, input.CapabilityOverride)
|
||||
}
|
||||
billingConfig := input.BillingConfig
|
||||
if len(billingConfig) == 0 {
|
||||
@ -355,6 +362,48 @@ func mergeObjects(base map[string]any, override map[string]any) map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeCapabilityObjects(base map[string]any, override map[string]any) map[string]any {
|
||||
out := cloneObject(base)
|
||||
for key, value := range override {
|
||||
baseChild, baseOK := out[key].(map[string]any)
|
||||
overrideChild, overrideOK := value.(map[string]any)
|
||||
if baseOK && overrideOK {
|
||||
out[key] = mergeCapabilityObjects(baseChild, overrideChild)
|
||||
continue
|
||||
}
|
||||
out[key] = cloneObjectValue(value)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneObject(values map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(values))
|
||||
for key, value := range values {
|
||||
out[key] = cloneObjectValue(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneObjectValue(value any) any {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
return cloneObject(typed)
|
||||
case []any:
|
||||
out := make([]any, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
out = append(out, cloneObjectValue(item))
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
return append([]string(nil), typed...)
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func emptyObjectIfNil(value map[string]any) map[string]any {
|
||||
if value == nil {
|
||||
return map[string]any{}
|
||||
|
||||
79
apps/api/internal/store/platform_models_test.go
Normal file
79
apps/api/internal/store/platform_models_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEffectivePlatformModelCapabilitiesDeepMergesNestedObjects(t *testing.T) {
|
||||
baseLevels := []any{"none", "minimal", "low"}
|
||||
baseTextGenerate := map[string]any{
|
||||
"max_context_tokens": 204800,
|
||||
"supportThinking": true,
|
||||
"thinkingEffortLevels": baseLevels,
|
||||
}
|
||||
overrideTextGenerate := map[string]any{
|
||||
"max_thinking_tokens": 131072,
|
||||
"supportThinking": false,
|
||||
"supportThinkingModeSwitch": true,
|
||||
}
|
||||
|
||||
capabilities := EffectivePlatformModelCapabilities(
|
||||
map[string]any{
|
||||
"originalTypes": []any{"text_generate"},
|
||||
"text_generate": baseTextGenerate,
|
||||
},
|
||||
map[string]any{
|
||||
"text_generate": overrideTextGenerate,
|
||||
},
|
||||
)
|
||||
|
||||
textGenerate, ok := capabilities["text_generate"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected text_generate capabilities object, got %#v", capabilities["text_generate"])
|
||||
}
|
||||
if textGenerate["max_context_tokens"] != 204800 {
|
||||
t.Fatalf("expected base max_context_tokens to be preserved, got %#v", textGenerate["max_context_tokens"])
|
||||
}
|
||||
if textGenerate["max_thinking_tokens"] != 131072 {
|
||||
t.Fatalf("expected override max_thinking_tokens to be applied, got %#v", textGenerate["max_thinking_tokens"])
|
||||
}
|
||||
if textGenerate["supportThinking"] != false {
|
||||
t.Fatalf("expected override supportThinking false to win, got %#v", textGenerate["supportThinking"])
|
||||
}
|
||||
if textGenerate["supportThinkingModeSwitch"] != true {
|
||||
t.Fatalf("expected override supportThinkingModeSwitch true, got %#v", textGenerate["supportThinkingModeSwitch"])
|
||||
}
|
||||
assertAnyStringList(t, textGenerate["thinkingEffortLevels"], []string{"none", "minimal", "low"})
|
||||
|
||||
baseTextGenerate["max_context_tokens"] = 1
|
||||
overrideTextGenerate["max_thinking_tokens"] = 2
|
||||
baseLevels[0] = "changed"
|
||||
|
||||
if textGenerate["max_context_tokens"] != 204800 {
|
||||
t.Fatalf("expected merged capabilities to be detached from base mutations, got %#v", textGenerate["max_context_tokens"])
|
||||
}
|
||||
if textGenerate["max_thinking_tokens"] != 131072 {
|
||||
t.Fatalf("expected merged capabilities to be detached from override mutations, got %#v", textGenerate["max_thinking_tokens"])
|
||||
}
|
||||
assertAnyStringList(t, textGenerate["thinkingEffortLevels"], []string{"none", "minimal", "low"})
|
||||
}
|
||||
|
||||
func assertAnyStringList(t *testing.T, got any, want []string) {
|
||||
t.Helper()
|
||||
items, ok := got.([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected string list %v, got %#v", want, got)
|
||||
}
|
||||
normalized := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
text, ok := item.(string)
|
||||
if !ok {
|
||||
t.Fatalf("expected string list %v, got non-string item %#v in %#v", want, item, got)
|
||||
}
|
||||
normalized = append(normalized, text)
|
||||
}
|
||||
if !slices.Equal(normalized, want) {
|
||||
t.Fatalf("expected string list %v, got %v", want, normalized)
|
||||
}
|
||||
}
|
||||
@ -150,6 +150,7 @@ type PlatformModel struct {
|
||||
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"`
|
||||
@ -806,13 +807,28 @@ 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, m.pricing_mode, COALESCE(m.discount_factor, 0)::float8,
|
||||
m.capability_override, m.capabilities, COALESCE(b.capabilities, '{}'::jsonb), 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"'), ''),
|
||||
m.enabled, m.created_at, m.updated_at
|
||||
FROM platform_models m
|
||||
JOIN integration_platforms p ON p.id = m.platform_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT catalog.capabilities
|
||||
FROM base_model_catalog catalog
|
||||
WHERE (m.base_model_id IS NOT NULL AND catalog.id = m.base_model_id)
|
||||
OR (
|
||||
catalog.provider_key = p.provider
|
||||
AND (
|
||||
catalog.provider_model_name = COALESCE(NULLIF(m.provider_model_name, ''), m.model_name)
|
||||
OR catalog.canonical_model_key = p.provider || ':' || COALESCE(NULLIF(m.provider_model_name, ''), m.model_name)
|
||||
)
|
||||
)
|
||||
ORDER BY CASE WHEN m.base_model_id IS NOT NULL AND catalog.id = m.base_model_id THEN 0 ELSE 1 END,
|
||||
catalog.updated_at DESC
|
||||
LIMIT 1
|
||||
) b ON true
|
||||
`+where+`
|
||||
ORDER BY m.model_type ASC, m.model_name ASC`, args...)
|
||||
if err != nil {
|
||||
@ -825,6 +841,7 @@ ORDER BY m.model_type ASC, m.model_name ASC`, args...)
|
||||
var model PlatformModel
|
||||
var capabilityOverride []byte
|
||||
var capabilities []byte
|
||||
var baseCapabilities []byte
|
||||
var billingConfigOverride []byte
|
||||
var billingConfig []byte
|
||||
var permissionConfig []byte
|
||||
@ -845,6 +862,7 @@ ORDER BY m.model_type ASC, m.model_name ASC`, args...)
|
||||
&model.DisplayName,
|
||||
&capabilityOverride,
|
||||
&capabilities,
|
||||
&baseCapabilities,
|
||||
&model.PricingMode,
|
||||
&model.DiscountFactor,
|
||||
&model.PricingRuleSetID,
|
||||
@ -864,6 +882,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.ModelType = decodeStringArray(modelTypeBytes)
|
||||
model.BillingConfigOverride = decodeObject(billingConfigOverride)
|
||||
model.BillingConfig = decodeObject(billingConfig)
|
||||
|
||||
@ -0,0 +1,165 @@
|
||||
-- 为内置文本模型补充上下文窗口配置,并为官方明确支持
|
||||
-- reasoning_effort / thinking_level 的模型补充可用推理深度。
|
||||
|
||||
CREATE OR REPLACE FUNCTION pg_temp._tmp_merge_model_capability_defaults(base jsonb, patch jsonb)
|
||||
RETURNS jsonb AS $$
|
||||
DECLARE
|
||||
out jsonb := COALESCE(base, '{}'::jsonb);
|
||||
patch_key text;
|
||||
patch_value jsonb;
|
||||
BEGIN
|
||||
IF patch IS NULL OR patch = '{}'::jsonb THEN
|
||||
RETURN out;
|
||||
END IF;
|
||||
|
||||
FOR patch_key, patch_value IN SELECT key, value FROM jsonb_each(patch) LOOP
|
||||
IF jsonb_typeof(patch_value) = 'object' AND jsonb_typeof(out -> patch_key) = 'object' THEN
|
||||
-- 仅补默认值,保留已有/人工配置的精确模型能力。
|
||||
out := jsonb_set(out, ARRAY[patch_key], patch_value || (out -> patch_key), true);
|
||||
ELSIF NOT out ? patch_key THEN
|
||||
out := jsonb_set(out, ARRAY[patch_key], patch_value, true);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
RETURN out;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION pg_temp._tmp_model_capability_patches()
|
||||
RETURNS TABLE(provider_key text, provider_model_name text, capability_patch jsonb) AS $$
|
||||
WITH patch_defs(patch_key, capability_patch) AS (
|
||||
VALUES
|
||||
('doubao_seed_2', '{
|
||||
"text_generate": {"max_context_tokens": 262144, "max_input_tokens": 262144, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportThinking": true, "supportThinkingModeSwitch": true},
|
||||
"image_analysis": {"max_context_tokens": 262144, "max_input_tokens": 262144, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportThinking": true, "supportThinkingModeSwitch": true},
|
||||
"tools_call": {"max_context_tokens": 262144, "max_input_tokens": 262144, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportTool": true, "supportThinking": true, "supportThinkingModeSwitch": true}
|
||||
}'::jsonb),
|
||||
('doubao_seed_18', '{
|
||||
"text_generate": {"max_context_tokens": 262144, "max_input_tokens": 229376, "max_output_tokens": 32768, "max_thinking_tokens": 32768, "supportThinking": true, "supportThinkingModeSwitch": true, "supportStructuredOutput": true},
|
||||
"image_analysis": {"max_context_tokens": 262144, "max_input_tokens": 229376, "max_output_tokens": 32768, "max_thinking_tokens": 32768, "supportThinking": true, "supportThinkingModeSwitch": true},
|
||||
"tools_call": {"max_context_tokens": 262144, "max_input_tokens": 229376, "max_output_tokens": 32768, "max_thinking_tokens": 32768, "supportTool": true, "supportThinking": true, "supportThinkingModeSwitch": true, "supportStructuredOutput": true}
|
||||
}'::jsonb),
|
||||
('glm_47', '{
|
||||
"text_generate": {"max_context_tokens": 204800, "max_input_tokens": 204800, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportThinking": true, "supportThinkingModeSwitch": true},
|
||||
"image_analysis": {"max_context_tokens": 204800, "max_input_tokens": 204800, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportThinking": true, "supportThinkingModeSwitch": true},
|
||||
"tools_call": {"max_context_tokens": 204800, "max_input_tokens": 204800, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportTool": true, "supportThinking": true, "supportThinkingModeSwitch": true}
|
||||
}'::jsonb),
|
||||
('glm_47_text_tools', '{
|
||||
"text_generate": {"max_context_tokens": 204800, "max_input_tokens": 204800, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportThinking": true, "supportThinkingModeSwitch": true},
|
||||
"tools_call": {"max_context_tokens": 204800, "max_input_tokens": 204800, "max_output_tokens": 131072, "max_thinking_tokens": 131072, "supportTool": true, "supportThinking": true, "supportThinkingModeSwitch": true}
|
||||
}'::jsonb),
|
||||
('gemini_3_text', '{
|
||||
"text_generate": {"thinkingEffortLevels": ["minimal", "low", "medium", "high"]},
|
||||
"tools_call": {"thinkingEffortLevels": ["minimal", "low", "medium", "high"]},
|
||||
"omni": {"thinkingEffortLevels": ["minimal", "low", "medium", "high"]}
|
||||
}'::jsonb),
|
||||
('deepseek_v4', '{
|
||||
"text_generate": {"thinkingEffortLevels": ["high", "max"]},
|
||||
"tools_call": {"thinkingEffortLevels": ["high", "max"]}
|
||||
}'::jsonb)
|
||||
),
|
||||
model_patches(provider_key, provider_model_name, patch_key) AS (
|
||||
VALUES
|
||||
('easyai', 'Doubao Seed 2.0 Pro', 'doubao_seed_2'),
|
||||
('easyai', 'Doubao Seed 2.0 Lite', 'doubao_seed_2'),
|
||||
('easyai', 'Doubao Seed 2.0 Mini', 'doubao_seed_2'),
|
||||
('easyai', 'Doubao Seed 2.0 Code Preview', 'doubao_seed_2'),
|
||||
('volces-openai', 'doubao-seed-2-0-pro-260215', 'doubao_seed_2'),
|
||||
('volces-openai', 'doubao-seed-2-0-lite-260215', 'doubao_seed_2'),
|
||||
('volces-openai', 'doubao-seed-2-0-mini-260215', 'doubao_seed_2'),
|
||||
('volces-openai', 'doubao-seed-2-0-code-preview-260215', 'doubao_seed_2'),
|
||||
|
||||
('easyai', 'Doubao Seed 1.8', 'doubao_seed_18'),
|
||||
('volces-openai', 'doubao-seed-1-8-251228', 'doubao_seed_18'),
|
||||
|
||||
('easyai', 'GLM-4.7', 'glm_47'),
|
||||
('volces-openai', 'glm-4-7-251222', 'glm_47'),
|
||||
('zhipu-openai', 'glm-4.7', 'glm_47_text_tools'),
|
||||
|
||||
('easyai', 'Gemini-3 Pro 预览版', 'gemini_3_text'),
|
||||
('easyai', 'Gemini-3 Flash 预览版', 'gemini_3_text'),
|
||||
('easyai', 'Gemini-3.1 Flash Lite 预览版', 'gemini_3_text'),
|
||||
('easyai', 'Gemini-3.1 Pro 预览版', 'gemini_3_text'),
|
||||
('gemini', 'gemini-3-pro-preview', 'gemini_3_text'),
|
||||
('gemini', 'gemini-3-flash-preview', 'gemini_3_text'),
|
||||
('gemini', 'gemini-3.1-flash-lite-preview', 'gemini_3_text'),
|
||||
('gemini', 'gemini-3.1-pro-preview', 'gemini_3_text'),
|
||||
('gemini-openai', 'gemini-3-pro-preview', 'gemini_3_text'),
|
||||
('gemini-openai', 'gemini-3-flash-preview', 'gemini_3_text'),
|
||||
('gemini-openai', 'gemini-3.1-flash-lite-preview', 'gemini_3_text'),
|
||||
('gemini-openai', 'gemini-3.1-pro-preview', 'gemini_3_text'),
|
||||
|
||||
('easyai', 'DeepSeek-V4-Pro', 'deepseek_v4'),
|
||||
('easyai', 'DeepSeek-V4-Flash', 'deepseek_v4'),
|
||||
('aliyun-bailian-openai', 'deepseek-v4-pro', 'deepseek_v4'),
|
||||
('aliyun-bailian-openai', 'deepseek-v4-flash', 'deepseek_v4'),
|
||||
('deepseek-openai', 'deepseek-v4-pro', 'deepseek_v4'),
|
||||
('deepseek-openai', 'deepseek-v4-flash', 'deepseek_v4')
|
||||
)
|
||||
SELECT model_patches.provider_key, model_patches.provider_model_name, patch_defs.capability_patch
|
||||
FROM model_patches
|
||||
JOIN patch_defs ON patch_defs.patch_key = model_patches.patch_key;
|
||||
$$ LANGUAGE sql;
|
||||
|
||||
UPDATE base_model_catalog b
|
||||
SET capabilities = pg_temp._tmp_merge_model_capability_defaults(b.capabilities, patches.capability_patch),
|
||||
default_snapshot = CASE
|
||||
WHEN COALESCE(b.default_snapshot, '{}'::jsonb) = '{}'::jsonb THEN b.default_snapshot
|
||||
WHEN jsonb_typeof(b.default_snapshot->'metadata'->'rawModel') = 'object' THEN jsonb_set(
|
||||
jsonb_set(
|
||||
b.default_snapshot,
|
||||
'{capabilities}',
|
||||
pg_temp._tmp_merge_model_capability_defaults(
|
||||
COALESCE(b.default_snapshot->'capabilities', '{}'::jsonb),
|
||||
patches.capability_patch
|
||||
),
|
||||
true
|
||||
),
|
||||
'{metadata,rawModel,capabilities}',
|
||||
pg_temp._tmp_merge_model_capability_defaults(
|
||||
COALESCE(b.default_snapshot->'metadata'->'rawModel'->'capabilities', '{}'::jsonb),
|
||||
patches.capability_patch
|
||||
),
|
||||
true
|
||||
)
|
||||
ELSE jsonb_set(
|
||||
b.default_snapshot,
|
||||
'{capabilities}',
|
||||
pg_temp._tmp_merge_model_capability_defaults(
|
||||
COALESCE(b.default_snapshot->'capabilities', '{}'::jsonb),
|
||||
patches.capability_patch
|
||||
),
|
||||
true
|
||||
)
|
||||
END,
|
||||
metadata = CASE
|
||||
WHEN jsonb_typeof(b.metadata->'rawModel') = 'object' THEN jsonb_set(
|
||||
b.metadata,
|
||||
'{rawModel,capabilities}',
|
||||
pg_temp._tmp_merge_model_capability_defaults(
|
||||
COALESCE(b.metadata->'rawModel'->'capabilities', '{}'::jsonb),
|
||||
patches.capability_patch
|
||||
),
|
||||
true
|
||||
)
|
||||
ELSE b.metadata
|
||||
END,
|
||||
updated_at = now()
|
||||
FROM pg_temp._tmp_model_capability_patches() patches
|
||||
WHERE b.provider_key = patches.provider_key
|
||||
AND b.provider_model_name = patches.provider_model_name
|
||||
AND b.catalog_type = 'system';
|
||||
|
||||
UPDATE platform_models m
|
||||
SET capabilities = pg_temp._tmp_merge_model_capability_defaults(m.capabilities, patches.capability_patch),
|
||||
updated_at = now()
|
||||
FROM integration_platforms p, pg_temp._tmp_model_capability_patches() patches
|
||||
WHERE m.platform_id = p.id
|
||||
AND p.provider = patches.provider_key
|
||||
AND (
|
||||
m.model_name = patches.provider_model_name
|
||||
OR m.provider_model_name = patches.provider_model_name
|
||||
);
|
||||
|
||||
DROP FUNCTION pg_temp._tmp_model_capability_patches();
|
||||
DROP FUNCTION pg_temp._tmp_merge_model_capability_defaults(jsonb, jsonb);
|
||||
Loading…
Reference in New Issue
Block a user