Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangbo 2026-05-26 23:21:21 +08:00
commit 1d3a4f1da9
6 changed files with 686 additions and 2 deletions

View File

@ -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
}
}

View 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)
}
}

View File

@ -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{}

View 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)
}
}

View File

@ -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)

View File

@ -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);