diff --git a/apps/api/internal/clients/chat_reasoning.go b/apps/api/internal/clients/chat_reasoning.go new file mode 100644 index 0000000..abb2b53 --- /dev/null +++ b/apps/api/internal/clients/chat_reasoning.go @@ -0,0 +1,283 @@ +package clients + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" +) + +const OpenAIReasoningEffortValidationMessage = "reasoning_effort must be one of: none, minimal, low, medium, high, xhigh" + +var ( + openAIReasoningEfforts = map[string]struct{}{ + "none": {}, + "minimal": {}, + "low": {}, + "medium": {}, + "high": {}, + "xhigh": {}, + } + volcesChatReasoningEfforts = map[string]struct{}{ + "minimal": {}, + "low": {}, + "medium": {}, + "high": {}, + } + zhipuReasoningEfforts = map[string]struct{}{ + "none": {}, + "minimal": {}, + "low": {}, + "medium": {}, + "high": {}, + "xhigh": {}, + } +) + +func ValidateOpenAIReasoningEffort(value any) error { + effort := normalizedReasoningString(value) + if effort == "" { + return nil + } + if isOpenAIReasoningEffort(effort) { + return nil + } + return &ClientError{Code: "invalid_parameter", Message: OpenAIReasoningEffortValidationMessage, Retryable: false} +} + +func applyOpenAIChatReasoningParams(body map[string]any, candidate store.RuntimeModelCandidate) { + effort := normalizedReasoningString(body["reasoning_effort"]) + if effort == "" || !isOpenAIReasoningEffort(effort) { + return + } + body["reasoning_effort"] = effort + + switch { + case isAliyunBailianOpenAI(candidate): + applyAliyunReasoning(body, candidate, effort) + case isDeepSeekOpenAI(candidate): + applyHighMaxThinkingReasoning(body, effort) + case isZhipuOpenAI(candidate): + applyZhipuReasoning(body, candidate, effort) + case isVolcesOpenAI(candidate): + applyVolcesReasoning(body, candidate, effort) + } +} + +func applyAliyunReasoning(body map[string]any, candidate store.RuntimeModelCandidate, effort string) { + defer delete(body, "thinking_budget_tokens") + + if effort == "none" { + body["enable_thinking"] = false + delete(body, "reasoning_effort") + return + } + + body["enable_thinking"] = true + model := chatReasoningModelName(body, candidate) + if isAliyunHighMaxReasoningModel(model) { + body["reasoning_effort"] = highMaxReasoningEffort(effort) + return + } + + delete(body, "reasoning_effort") + if isAliyunThinkingBudgetModel(model) { + if budget, ok := positiveIntFromAny(body["thinking_budget_tokens"]); ok { + body["thinking_budget"] = budget + } + } +} + +func applyHighMaxThinkingReasoning(body map[string]any, effort string) { + if effort == "none" { + body["thinking"] = map[string]any{"type": "disabled"} + delete(body, "reasoning_effort") + return + } + body["thinking"] = map[string]any{"type": "enabled"} + body["reasoning_effort"] = highMaxReasoningEffort(effort) +} + +func applyZhipuReasoning(body map[string]any, candidate store.RuntimeModelCandidate, effort string) { + if effort == "none" { + body["thinking"] = map[string]any{"type": "disabled"} + delete(body, "reasoning_effort") + return + } + body["thinking"] = map[string]any{"type": "enabled"} + if !isZhipuReasoningEffortModel(chatReasoningModelName(body, candidate)) { + delete(body, "reasoning_effort") + return + } + if mapped := zhipuReasoningEffort(effort); mapped != "" { + body["reasoning_effort"] = mapped + return + } + delete(body, "reasoning_effort") +} + +func applyVolcesReasoning(body map[string]any, _ store.RuntimeModelCandidate, effort string) { + if effort == "none" { + body["thinking"] = map[string]any{"type": "disabled"} + delete(body, "reasoning_effort") + return + } + + body["thinking"] = map[string]any{"type": "enabled"} + if mapped := volcesChatReasoningEffort(effort); mapped != "" { + body["reasoning_effort"] = mapped + return + } + delete(body, "reasoning_effort") +} + +func normalizedReasoningString(value any) string { + if value == nil { + return "" + } + text, ok := value.(string) + if !ok { + text = fmt.Sprint(value) + } + return strings.ToLower(strings.TrimSpace(text)) +} + +func isOpenAIReasoningEffort(effort string) bool { + _, ok := openAIReasoningEfforts[effort] + return ok +} + +func chatReasoningModelName(body map[string]any, candidate store.RuntimeModelCandidate) string { + for _, value := range []any{ + body["model"], + candidate.ProviderModelName, + candidate.ModelName, + candidate.ModelAlias, + } { + if text := normalizedReasoningString(value); text != "" { + return text + } + } + return "" +} + +func providerCode(candidate store.RuntimeModelCandidate) string { + return normalizedReasoningString(candidate.Provider) +} + +func baseURLCode(candidate store.RuntimeModelCandidate) string { + return normalizedReasoningString(candidate.BaseURL) +} + +func isAliyunBailianOpenAI(candidate store.RuntimeModelCandidate) bool { + provider := providerCode(candidate) + baseURL := baseURLCode(candidate) + return provider == "aliyun-bailian-openai" || strings.Contains(baseURL, "dashscope.") +} + +func isVolcesOpenAI(candidate store.RuntimeModelCandidate) bool { + provider := providerCode(candidate) + baseURL := baseURLCode(candidate) + return provider == "volces-openai" || strings.Contains(baseURL, "volces.com") || strings.Contains(baseURL, "byteplus.com") +} + +func isDeepSeekOpenAI(candidate store.RuntimeModelCandidate) bool { + provider := providerCode(candidate) + baseURL := baseURLCode(candidate) + return provider == "deepseek-openai" || strings.Contains(baseURL, "api.deepseek.com") +} + +func isZhipuOpenAI(candidate store.RuntimeModelCandidate) bool { + provider := providerCode(candidate) + baseURL := baseURLCode(candidate) + return provider == "zhipu-openai" || strings.Contains(baseURL, "bigmodel.cn") || strings.Contains(baseURL, "api.z.ai") +} + +func isAliyunHighMaxReasoningModel(model string) bool { + return strings.Contains(model, "deepseek-v4") || strings.HasPrefix(model, "glm-") +} + +func isAliyunThinkingBudgetModel(model string) bool { + return strings.Contains(model, "qwen") || strings.Contains(model, "qwq") || strings.Contains(model, "qvq") || strings.Contains(model, "kimi") +} + +func isVolcesReasoningEffortModel(model string) bool { + return strings.HasPrefix(model, "doubao-seed-2-") +} + +func isZhipuReasoningEffortModel(model string) bool { + return model == "" || strings.HasPrefix(model, "glm-5.2") || strings.HasPrefix(model, "glm-5-2") +} + +func highMaxReasoningEffort(effort string) string { + if effort == "xhigh" { + return "max" + } + return "high" +} + +func zhipuReasoningEffort(effort string) string { + if _, ok := zhipuReasoningEfforts[effort]; ok { + return effort + } + return "" +} + +func volcesChatReasoningEffort(effort string) string { + switch effort { + case "none": + return "minimal" + case "xhigh": + return "high" + default: + if _, ok := volcesChatReasoningEfforts[effort]; ok { + return effort + } + return "" + } +} + +func positiveIntFromAny(value any) (int, bool) { + if value == nil { + return 0, false + } + var number float64 + switch typed := value.(type) { + case int: + number = float64(typed) + case int64: + number = float64(typed) + case float64: + number = typed + case float32: + number = float64(typed) + case jsonNumber: + parsed, err := typed.Float64() + if err != nil { + return 0, false + } + number = parsed + case string: + parsed := normalizedReasoningString(typed) + if parsed == "" { + return 0, false + } + value, err := strconv.ParseFloat(parsed, 64) + if err != nil { + return 0, false + } + number = value + default: + return 0, false + } + if math.IsNaN(number) || math.IsInf(number, 0) || number <= 0 { + return 0, false + } + return int(math.Floor(number)), true +} + +type jsonNumber interface { + Float64() (float64, error) +} diff --git a/apps/api/internal/clients/clients_test.go b/apps/api/internal/clients/clients_test.go index 4096745..2843d0e 100644 --- a/apps/api/internal/clients/clients_test.go +++ b/apps/api/internal/clients/clients_test.go @@ -343,6 +343,190 @@ func TestOpenAIClientChatContract(t *testing.T) { } } +func TestOpenAIClientChatReasoningParamsByProvider(t *testing.T) { + cases := []struct { + name string + provider string + model string + baseURL string + body map[string]any + assertion func(t *testing.T, body map[string]any) + }{ + { + name: "generic openai keeps openai reasoning effort", + provider: "openai", + model: "gpt-5.5", + body: map[string]any{ + "reasoning_effort": "xhigh", + }, + assertion: func(t *testing.T, body map[string]any) { + if body["reasoning_effort"] != "xhigh" { + t.Fatalf("generic openai should keep xhigh, got %+v", body) + } + if _, ok := body["thinking"]; ok { + t.Fatalf("generic openai should not receive thinking object: %+v", body) + } + }, + }, + { + name: "aliyun qwen enables thinking and maps budget", + provider: "aliyun-bailian-openai", + model: "qwen3.7-plus", + body: map[string]any{ + "reasoning_effort": "high", + "thinking_budget_tokens": 4096, + }, + assertion: func(t *testing.T, body map[string]any) { + if body["enable_thinking"] != true || body["thinking_budget"] != float64(4096) { + t.Fatalf("aliyun qwen should use enable_thinking/thinking_budget, got %+v", body) + } + if _, ok := body["reasoning_effort"]; ok { + t.Fatalf("aliyun qwen should not receive reasoning_effort: %+v", body) + } + if _, ok := body["thinking_budget_tokens"]; ok { + t.Fatalf("internal thinking_budget_tokens should be removed: %+v", body) + } + }, + }, + { + name: "aliyun qwen disables thinking for none", + provider: "aliyun-bailian-openai", + model: "qwen3.7-plus", + body: map[string]any{ + "reasoning_effort": "none", + }, + assertion: func(t *testing.T, body map[string]any) { + if body["enable_thinking"] != false { + t.Fatalf("aliyun qwen none should disable thinking, got %+v", body) + } + if _, ok := body["reasoning_effort"]; ok { + t.Fatalf("aliyun qwen none should remove reasoning_effort: %+v", body) + } + }, + }, + { + name: "same deepseek model on aliyun uses aliyun protocol", + provider: "aliyun-bailian-openai", + model: "deepseek-v4", + body: map[string]any{ + "reasoning_effort": "xhigh", + }, + assertion: func(t *testing.T, body map[string]any) { + if body["enable_thinking"] != true || body["reasoning_effort"] != "max" { + t.Fatalf("aliyun deepseek should use enable_thinking and max, got %+v", body) + } + }, + }, + { + name: "deepseek official uses thinking object and high max effort", + provider: "deepseek-openai", + model: "deepseek-v4", + body: map[string]any{ + "reasoning_effort": "xhigh", + }, + assertion: func(t *testing.T, body map[string]any) { + thinking, _ := body["thinking"].(map[string]any) + if thinking["type"] != "enabled" || body["reasoning_effort"] != "max" { + t.Fatalf("deepseek official should use thinking enabled and max, got %+v", body) + } + }, + }, + { + name: "volces seed none disables thinking", + provider: "volces-openai", + model: "doubao-seed-2-0-pro-260215", + body: map[string]any{ + "reasoning_effort": "none", + }, + assertion: func(t *testing.T, body map[string]any) { + thinking, _ := body["thinking"].(map[string]any) + if thinking["type"] != "disabled" { + t.Fatalf("volces seed none should disable thinking, got %+v", body) + } + if _, ok := body["reasoning_effort"]; ok { + t.Fatalf("volces seed none should remove reasoning_effort: %+v", body) + } + }, + }, + { + name: "volces seed enables thinking and maps xhigh", + provider: "volces-openai", + model: "doubao-seed-2-0-pro-260215", + body: map[string]any{ + "reasoning_effort": "xhigh", + }, + assertion: func(t *testing.T, body map[string]any) { + thinking, _ := body["thinking"].(map[string]any) + if thinking["type"] != "enabled" || body["reasoning_effort"] != "high" { + t.Fatalf("volces seed xhigh should enable thinking and map to high, got %+v", body) + } + }, + }, + { + name: "zhipu preserves xhigh for glm 5.2", + provider: "zhipu-openai", + model: "glm-5.2", + body: map[string]any{ + "reasoning_effort": "xhigh", + }, + assertion: func(t *testing.T, body map[string]any) { + thinking, _ := body["thinking"].(map[string]any) + if thinking["type"] != "enabled" || body["reasoning_effort"] != "xhigh" { + t.Fatalf("zhipu glm should use thinking enabled and preserve xhigh, got %+v", body) + } + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var captured map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&captured); err != nil { + t.Fatalf("decode request: %v", err) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "chatcmpl-reasoning", + "object": "chat.completion", + "model": captured["model"], + "choices": []any{map[string]any{ + "message": map[string]any{"role": "assistant", "content": "ok"}, + }}, + }) + })) + defer server.Close() + + body := map[string]any{ + "messages": []any{map[string]any{"role": "user", "content": "ping"}}, + } + for key, value := range tc.body { + body[key] = value + } + baseURL := tc.baseURL + if baseURL == "" { + baseURL = server.URL + } + + _, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{ + Kind: "chat.completions", + Model: tc.model, + Body: body, + Candidate: store.RuntimeModelCandidate{ + Provider: tc.provider, + BaseURL: baseURL, + ProviderModelName: tc.model, + ModelName: tc.model, + Credentials: map[string]any{"apiKey": "test-key"}, + }, + }) + if err != nil { + t.Fatalf("run openai client: %v", err) + } + tc.assertion(t, captured) + }) + } +} + func TestUsageFromOpenAIUsageTracksKnownCachedInputZero(t *testing.T) { knownZero := usageFromOpenAIUsage(map[string]any{ "prompt_tokens": 100, diff --git a/apps/api/internal/clients/openai.go b/apps/api/internal/clients/openai.go index 95eada7..c973f69 100644 --- a/apps/api/internal/clients/openai.go +++ b/apps/api/internal/clients/openai.go @@ -27,6 +27,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error body := cloneBody(request.Body) if request.Kind == "chat.completions" { body = NormalizeChatCompletionRequestBody(body) + applyOpenAIChatReasoningParams(body, request.Candidate) } body["model"] = upstreamModelName(request.Candidate) stream := openAIEndpointSupportsStream(request.Kind) && (request.Stream || boolValue(body, "stream")) diff --git a/apps/api/internal/httpapi/chat_completions_mode_test.go b/apps/api/internal/httpapi/chat_completions_mode_test.go index 54681e8..d28c873 100644 --- a/apps/api/internal/httpapi/chat_completions_mode_test.go +++ b/apps/api/internal/httpapi/chat_completions_mode_test.go @@ -138,6 +138,27 @@ func TestWriteCompatibleTaskResponseReturnsJSONWhenStreamIsFalse(t *testing.T) { } } +func TestWriteCompatibleTaskResponseMapsInvalidParameterToBadRequest(t *testing.T) { + executor := &fakeTaskExecutor{ + runErr: &clients.ClientError{ + Code: "invalid_parameter", + Message: "reasoning_effort must be one of: none, minimal, low, medium, high, xhigh", + Retryable: false, + }, + } + req := httptest.NewRequest(http.MethodPost, "/api/v1/chat/completions", nil) + recorder := httptest.NewRecorder() + + writeCompatibleTaskResponse(context.Background(), recorder, req, executor, "chat.completions", "gpt-test", store.GatewayTask{ID: "task-test"}, &auth.User{}, false, false) + + if recorder.Code != http.StatusBadRequest { + t.Fatalf("status=%d want=%d body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String()) + } + if !strings.Contains(recorder.Body.String(), "invalid_parameter") { + t.Fatalf("response should include invalid_parameter code: %s", recorder.Body.String()) + } +} + func TestWriteCompatibleTaskResponseReturnsSSEWhenStreamIsTrue(t *testing.T) { executor := &fakeTaskExecutor{ deltas: []clients.StreamDeltaEvent{{Text: "hel"}, {Text: "lo"}}, @@ -284,15 +305,22 @@ type fakeTaskExecutor struct { streamCalls int deltas []clients.StreamDeltaEvent output map[string]any + runErr error } func (f *fakeTaskExecutor) Execute(context.Context, store.GatewayTask, *auth.User) (runner.Result, error) { f.executeCalls++ + if f.runErr != nil { + return runner.Result{}, f.runErr + } return runner.Result{Output: f.output}, nil } func (f *fakeTaskExecutor) ExecuteStream(_ context.Context, _ store.GatewayTask, _ *auth.User, onDelta clients.StreamDelta) (runner.Result, error) { f.streamCalls++ + if f.runErr != nil { + return runner.Result{}, f.runErr + } for _, delta := range f.deltas { if err := onDelta(delta); err != nil { return runner.Result{}, err diff --git a/apps/api/internal/httpapi/handlers.go b/apps/api/internal/httpapi/handlers.go index 6d6adda..5cf9575 100644 --- a/apps/api/internal/httpapi/handlers.go +++ b/apps/api/internal/httpapi/handlers.go @@ -1306,7 +1306,7 @@ func scopeForTaskKind(kind string) string { func statusFromRunError(err error) int { switch { - case clients.ErrorCode(err) == "bad_request" || clients.ErrorCode(err) == "cloned_voice_expired" || clients.ErrorCode(err) == "cloned_voice_unavailable" || clients.ErrorCode(err) == "cloned_voice_platform_unavailable" || clients.ErrorCode(err) == "unsupported_operation" || clients.ErrorCode(err) == "invalid_proxy": + case clients.ErrorCode(err) == "bad_request" || clients.ErrorCode(err) == "invalid_parameter" || clients.ErrorCode(err) == "cloned_voice_expired" || clients.ErrorCode(err) == "cloned_voice_unavailable" || clients.ErrorCode(err) == "cloned_voice_platform_unavailable" || clients.ErrorCode(err) == "unsupported_operation" || clients.ErrorCode(err) == "invalid_proxy": return http.StatusBadRequest case clients.ErrorCode(err) == "cloned_voice_not_found": return http.StatusNotFound diff --git a/apps/api/internal/httpapi/openapi_models.go b/apps/api/internal/httpapi/openapi_models.go index 27ec9da..200614f 100644 --- a/apps/api/internal/httpapi/openapi_models.go +++ b/apps/api/internal/httpapi/openapi_models.go @@ -190,7 +190,7 @@ type TaskRequest struct { Stream bool `json:"stream,omitempty" example:"false"` RunMode string `json:"runMode,omitempty" example:"simulation"` MaxTokens int `json:"max_tokens,omitempty" example:"512"` - // ReasoningEffort 推理深度,OpenAI-compatible 请求字段;开放字符串,取值随 provider 和模型能力而定,常见值为 none、minimal、low、medium、high、xhigh,也可配置 max 等供应商自定义值。 + // ReasoningEffort 推理强度,OpenAI-compatible 请求字段;仅支持 none、minimal、low、medium、high、xhigh。供应商自定义取值由网关按平台适配。 ReasoningEffort string `json:"reasoning_effort,omitempty" example:"medium"` Size string `json:"size,omitempty" example:"1024x1024"` Duration int `json:"duration,omitempty" example:"5"` @@ -216,7 +216,7 @@ type ChatCompletionRequest struct { Messages []ChatMessage `json:"messages"` Temperature float64 `json:"temperature,omitempty" example:"0.7"` MaxTokens int `json:"max_tokens,omitempty" example:"512"` - // ReasoningEffort 推理深度,OpenAI-compatible 请求字段;开放字符串,取值随 provider 和模型能力而定,常见值为 none、minimal、low、medium、high、xhigh,也可配置 max 等供应商自定义值。 + // ReasoningEffort 推理强度,OpenAI-compatible 请求字段;仅支持 none、minimal、low、medium、high、xhigh。供应商自定义取值由网关按平台适配。 ReasoningEffort string `json:"reasoning_effort,omitempty" example:"medium"` Stream bool `json:"stream,omitempty" example:"false"` RunMode string `json:"runMode,omitempty" example:"simulation"` diff --git a/apps/api/internal/runner/service.go b/apps/api/internal/runner/service.go index 2e7255d..fccc7d2 100644 --- a/apps/api/internal/runner/service.go +++ b/apps/api/internal/runner/service.go @@ -1295,6 +1295,9 @@ func validateRequest(kind string, body map[string]any) error { if body["messages"] == nil { return errors.New("messages is required") } + if err := clients.ValidateOpenAIReasoningEffort(body["reasoning_effort"]); err != nil { + return err + } case "responses": if body["input"] == nil && body["messages"] == nil { return errors.New("input or messages is required") diff --git a/apps/api/internal/runner/service_test.go b/apps/api/internal/runner/service_test.go index b61e1b3..db21854 100644 --- a/apps/api/internal/runner/service_test.go +++ b/apps/api/internal/runner/service_test.go @@ -2,6 +2,7 @@ package runner import ( "context" + "strings" "testing" "github.com/easyai/easyai-ai-gateway/apps/api/internal/clients" @@ -14,6 +15,37 @@ func (namedClient) Run(context.Context, clients.Request) (clients.Response, erro return clients.Response{}, nil } +func TestValidateRequestAcceptsOpenAIReasoningEffort(t *testing.T) { + for _, effort := range []string{"none", "minimal", "low", "medium", "high", "xhigh"} { + t.Run(effort, func(t *testing.T) { + err := validateRequest("chat.completions", map[string]any{ + "messages": []any{map[string]any{"role": "user", "content": "ping"}}, + "reasoning_effort": effort, + }) + if err != nil { + t.Fatalf("expected %s to be accepted: %v", effort, err) + } + }) + } +} + +func TestValidateRequestRejectsNonOpenAIReasoningEffort(t *testing.T) { + for _, effort := range []string{"max", "auto"} { + t.Run(effort, func(t *testing.T) { + err := validateRequest("chat.completions", map[string]any{ + "messages": []any{map[string]any{"role": "user", "content": "ping"}}, + "reasoning_effort": effort, + }) + if err == nil { + t.Fatalf("expected %s to be rejected", effort) + } + if clients.ErrorCode(err) != "invalid_parameter" || !strings.Contains(err.Error(), clients.OpenAIReasoningEffortValidationMessage) { + t.Fatalf("unexpected validation error for %s: %T %v", effort, err, err) + } + }) + } +} + func TestClientForMapsGoogleGeminiSpecToGeminiClient(t *testing.T) { service := &Service{clients: map[string]clients.Client{ "gemini": namedClient("gemini"), diff --git a/apps/api/internal/store/candidates.go b/apps/api/internal/store/candidates.go index e15d92f..e1c66a4 100644 --- a/apps/api/internal/store/candidates.go +++ b/apps/api/internal/store/candidates.go @@ -160,18 +160,17 @@ WHERE p.status = 'enabled' ) ) OR ( - COALESCE(m.model_alias, '') = '' - AND ( - m.model_name = $1::text - OR b.canonical_model_key = $1::text - OR b.provider_model_name = $1::text - OR ( - NULLIF($3::text, '') IS NOT NULL - AND ( - regexp_replace(COALESCE(m.model_name, ''), '[[:space:]]+', '', 'g') = $3::text - OR regexp_replace(COALESCE(b.canonical_model_key, ''), '[[:space:]]+', '', 'g') = $3::text - OR regexp_replace(COALESCE(b.provider_model_name, ''), '[[:space:]]+', '', 'g') = $3::text - ) + m.model_name = $1::text + OR COALESCE(NULLIF(m.provider_model_name, ''), m.model_name) = $1::text + OR b.canonical_model_key = $1::text + OR b.provider_model_name = $1::text + OR ( + NULLIF($3::text, '') IS NOT NULL + AND ( + regexp_replace(COALESCE(m.model_name, ''), '[[:space:]]+', '', 'g') = $3::text + OR regexp_replace(COALESCE(NULLIF(m.provider_model_name, ''), m.model_name), '[[:space:]]+', '', 'g') = $3::text + OR regexp_replace(COALESCE(b.canonical_model_key, ''), '[[:space:]]+', '', 'g') = $3::text + OR regexp_replace(COALESCE(b.provider_model_name, ''), '[[:space:]]+', '', 'g') = $3::text ) ) ) diff --git a/apps/web/src/pages/admin/BaseModelCapabilityEditor.tsx b/apps/web/src/pages/admin/BaseModelCapabilityEditor.tsx index 36582d0..4555153 100644 --- a/apps/web/src/pages/admin/BaseModelCapabilityEditor.tsx +++ b/apps/web/src/pages/admin/BaseModelCapabilityEditor.tsx @@ -47,7 +47,7 @@ const textFields: FieldDefinition[] = [ { key: 'max_input_tokens', label: '最大输入 Token', placeholder: '64000', type: 'number' }, { key: 'max_output_tokens', label: '最大输出 Token', placeholder: '8192', type: 'number' }, { key: 'max_thinking_tokens', label: '最大思考 Token', placeholder: '32768', type: 'number' }, - { key: 'thinkingEffortLevels', label: '推理深度', hint: '声明模型支持的 reasoning_effort 取值,可填写 max 等供应商自定义值', placeholder: 'none, minimal, low, medium, high, xhigh, max', type: 'list' }, + { key: 'thinkingEffortLevels', label: '推理深度', hint: '声明模型支持的 OpenAI reasoning_effort 取值,平台差异由网关适配', placeholder: 'none, minimal, low, medium, high, xhigh', type: 'list' }, ]; const embeddingFields: FieldDefinition[] = [ @@ -536,7 +536,7 @@ const imageAspectRatioOptions = [ '7:4', '4:7', ]; -const thinkingEffortOptions = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']; +const thinkingEffortOptions = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']; const omniVideoModeOptions = ['text_to_video', 'image_reference', 'element_reference', 'first_last_frame', 'video_reference', 'video_edit', 'multi_shot']; const durationOptionValues = ['1', '2', '3', '4', '5', '6', '8', '10', '15', '20', '25', '30']; const exclusiveCapabilityFields: Record = {