fix(api): 补全模型能力继承与响应推导
- 合并 base model 默认能力与平台覆盖项 - 在模型响应中补齐 text_generate 的上下文与推理能力字段 - 为相关逻辑补充测试和迁移脚本
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user