mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
fix(model): 支持不设置推理强度
This commit is contained in:
@@ -63,6 +63,79 @@ func TestOpenAIResponsesRequestsReasoningSummary(t *testing.T) {
|
||||
assertOpenAIEventKindCount(t, events, ModelEventKindTextDelta, 1)
|
||||
}
|
||||
|
||||
func TestOpenAIResponsesOmitsReasoningWhenEffortBlank(t *testing.T) {
|
||||
var requestBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := json.NewDecoder(request.Body).Decode(&requestBody); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = fmt.Fprint(writer, "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp-1\",\"model\":\"grok-composer-2.5-fast\",\"status\":\"completed\",\"output_text\":\"done\"}}\n\n")
|
||||
_, _ = fmt.Fprint(writer, "data: [DONE]\n\n")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
adapter := &OpenAIAdapter{client: server.Client()}
|
||||
err := adapter.Stream(context.Background(), StreamRequest{
|
||||
RequestID: "request-1",
|
||||
RunID: "run-1",
|
||||
ModelCallID: "model-call-1",
|
||||
BaseURL: server.URL,
|
||||
APIKey: "test-key",
|
||||
ProviderModelID: "grok-composer-2.5-fast",
|
||||
OpenAIEndpoint: "/v1/responses",
|
||||
Messages: []Message{{Role: "user", Content: "hello"}},
|
||||
MaxTokens: 128,
|
||||
}, func(ModelEvent) error { return nil })
|
||||
if err != nil {
|
||||
t.Fatalf("stream failed: %v", err)
|
||||
}
|
||||
|
||||
if _, exists := requestBody["reasoning"]; exists {
|
||||
t.Fatalf("reasoning should be omitted when effort is blank: %#v", requestBody["reasoning"])
|
||||
}
|
||||
if _, exists := requestBody["reasoning_effort"]; exists {
|
||||
t.Fatalf("reasoning_effort should be omitted when effort is blank: %#v", requestBody["reasoning_effort"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIChatCompletionsOmitsReasoningWhenEffortBlank(t *testing.T) {
|
||||
var requestBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := json.NewDecoder(request.Body).Decode(&requestBody); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = fmt.Fprint(writer, "data: {\"model\":\"grok-composer-2.5-fast\",\"choices\":[{\"delta\":{\"content\":\"done\"},\"finish_reason\":\"stop\"}]}\n\n")
|
||||
_, _ = fmt.Fprint(writer, "data: [DONE]\n\n")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
adapter := &OpenAIAdapter{client: server.Client()}
|
||||
err := adapter.Stream(context.Background(), StreamRequest{
|
||||
RequestID: "request-1",
|
||||
RunID: "run-1",
|
||||
ModelCallID: "model-call-1",
|
||||
BaseURL: server.URL,
|
||||
APIKey: "test-key",
|
||||
ProviderModelID: "grok-composer-2.5-fast",
|
||||
OpenAIEndpoint: "/v1/chat/completions",
|
||||
Messages: []Message{{Role: "user", Content: "hello"}},
|
||||
MaxTokens: 128,
|
||||
}, func(ModelEvent) error { return nil })
|
||||
if err != nil {
|
||||
t.Fatalf("stream failed: %v", err)
|
||||
}
|
||||
|
||||
for _, field := range []string{"reasoning_effort", "reasoning", "include"} {
|
||||
if value, exists := requestBody[field]; exists {
|
||||
t.Fatalf("%s should be omitted when effort is blank: %#v", field, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIChatCompletionsIgnoresBlankFinishReason(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Content-Type", "text/event-stream")
|
||||
|
||||
@@ -1,10 +1,64 @@
|
||||
package modeladapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
type recordingModelAdapter struct {
|
||||
request StreamRequest
|
||||
}
|
||||
|
||||
func (adapter *recordingModelAdapter) Stream(_ context.Context, req StreamRequest, _ func(ModelEvent) error) error {
|
||||
adapter.request = req
|
||||
return nil
|
||||
}
|
||||
|
||||
type staticChannelResolver struct {
|
||||
channel *legacyruntime.ResolvedChannel
|
||||
}
|
||||
|
||||
func (resolver staticChannelResolver) SelectChannelForModel(context.Context, string) (*legacyruntime.ResolvedChannel, error) {
|
||||
return resolver.channel, nil
|
||||
}
|
||||
|
||||
func (staticChannelResolver) ProviderStreamIdleTimeout(context.Context) time.Duration {
|
||||
return time.Second
|
||||
}
|
||||
|
||||
func TestRouterRuntimeDisabledClearsReasoningEffort(t *testing.T) {
|
||||
openAI := &recordingModelAdapter{}
|
||||
router := &Router{
|
||||
openai: openAI,
|
||||
resolver: staticChannelResolver{channel: &legacyruntime.ResolvedChannel{
|
||||
ID: "channel-a",
|
||||
Provider: "openai",
|
||||
Model: "grok-composer-2.5-fast",
|
||||
ReasoningEffort: "medium",
|
||||
}},
|
||||
}
|
||||
requestKnobs := map[string]any{"reasoning_effort": "medium"}
|
||||
|
||||
err := router.Stream(context.Background(), StreamRequest{
|
||||
ModelID: "channel-a",
|
||||
ThinkingEffort: "disabled",
|
||||
RequestKnobs: requestKnobs,
|
||||
}, func(ModelEvent) error { return nil })
|
||||
if err != nil {
|
||||
t.Fatalf("Stream returned error: %v", err)
|
||||
}
|
||||
if got := openAI.request.ReasoningEffort; got != "" {
|
||||
t.Fatalf("ReasoningEffort = %q, want blank", got)
|
||||
}
|
||||
if _, exists := openAI.request.RequestKnobs["reasoning_effort"]; exists {
|
||||
t.Fatalf("reasoning_effort knob should be removed: %#v", openAI.request.RequestKnobs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeProviderMessagesMergesLegacyAssistantTextAndToolCallTurnsIdempotently(t *testing.T) {
|
||||
input := []Message{
|
||||
{
|
||||
|
||||
@@ -141,8 +141,8 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
|
||||
return nil, errors.New("模型适配器 tooltipData 不能为空")
|
||||
case next.ModelID == "":
|
||||
return nil, errors.New("模型适配器 modelID 不能为空")
|
||||
case next.Type == "openai" && next.ReasoningEffort == "":
|
||||
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh、max")
|
||||
case next.Type == "openai" && !isSupportedReasoningEffort(next.ReasoningEffort):
|
||||
return nil, errors.New("模型适配器 reasoningEffort 仅支持空值、low、medium、high、xhigh、max")
|
||||
case next.Type == "openai" && next.OpenAIEndpoint == "":
|
||||
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses、/v1/chat/completions 或 /custom(自定义路径)")
|
||||
case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
|
||||
@@ -224,13 +224,15 @@ func validateHeadersJSON(value string) error {
|
||||
}
|
||||
|
||||
func normalizeReasoningEffort(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "medium":
|
||||
return "medium"
|
||||
case "low", "high", "xhigh", "max":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func isSupportedReasoningEffort(value string) bool {
|
||||
switch value {
|
||||
case "", "low", "medium", "high", "xhigh", "max":
|
||||
return true
|
||||
default:
|
||||
return ""
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,3 +58,25 @@ func TestNormalizeModelAdapterConfigsUsesStableExplicitSort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeModelAdapterConfigsAllowsBlankReasoningEffort(t *testing.T) {
|
||||
adapter := testModelAdapter("non-reasoning-model", 1)
|
||||
adapter.ReasoningEffort = ""
|
||||
|
||||
adapters, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{adapter})
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizeModelAdapterConfigs returned error: %v", err)
|
||||
}
|
||||
if got := adapters[0].ReasoningEffort; got != "" {
|
||||
t.Fatalf("ReasoningEffort = %q, want blank", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeModelAdapterConfigsRejectsUnknownReasoningEffort(t *testing.T) {
|
||||
adapter := testModelAdapter("invalid-reasoning-effort", 1)
|
||||
adapter.ReasoningEffort = "unsupported"
|
||||
|
||||
if _, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{adapter}); err == nil {
|
||||
t.Fatal("NormalizeModelAdapterConfigs should reject an unknown reasoning effort")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ func defaultThinkingEffortForAdapter(adapter legacyruntime.ModelAdapterConfig) s
|
||||
if strings.EqualFold(strings.TrimSpace(adapter.Type), "anthropic") {
|
||||
return normalizeAvailableModelThinkingEffort(adapter.AnthropicThinkingEffort, true, "xhigh")
|
||||
}
|
||||
return normalizeAvailableModelThinkingEffort(adapter.ReasoningEffort, true, "medium")
|
||||
return normalizeAvailableModelThinkingEffort(adapter.ReasoningEffort, true, "disabled")
|
||||
}
|
||||
|
||||
func normalizeAvailableModelThinkingEffort(raw string, allowMax bool, fallback string) string {
|
||||
|
||||
@@ -28,6 +28,40 @@ func TestBuildCLIModelDetailsPreservesChannelMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultThinkingEffortForOpenAIAdapterUsesDisabledWhenUnset(t *testing.T) {
|
||||
adapter := legacyruntime.ModelAdapterConfig{Type: "openai", ReasoningEffort: ""}
|
||||
|
||||
if got := defaultThinkingEffortForAdapter(adapter); got != "disabled" {
|
||||
t.Fatalf("default thinking effort = %q, want disabled", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAvailableModelEntriesUsesDisabledVariantWhenReasoningEffortUnset(t *testing.T) {
|
||||
entries := buildAvailableModelEntries([]legacyruntime.ModelAdapterConfig{{
|
||||
ID: "channel-a",
|
||||
DisplayName: "Model A",
|
||||
ModelID: "model-a",
|
||||
Type: "openai",
|
||||
}})
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("entry count = %d, want 1", len(entries))
|
||||
}
|
||||
|
||||
variants, ok := entries[0]["variants"].([]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("variants type = %T, want []map[string]any", entries[0]["variants"])
|
||||
}
|
||||
if len(variants) == 0 {
|
||||
t.Fatal("variants should not be empty")
|
||||
}
|
||||
if got := variants[0]["variantStringRepresentation"]; got != "channel-a:disabled" {
|
||||
t.Fatalf("first variant representation = %#v, want channel-a:disabled", got)
|
||||
}
|
||||
if got := variants[0]["isDefaultNonMaxConfig"]; got != true {
|
||||
t.Fatalf("disabled variant default flag = %#v, want true", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeCLIModelsUsesAgentModelDetailsWireFormat(t *testing.T) {
|
||||
payload := map[string]any{"models": buildCLIModelDetails([]legacyruntime.ModelAdapterConfig{{ID: "channel-a", DisplayName: "Model A", APIKey: "provider-secret", BaseURL: "https://provider.example/v1"}})}
|
||||
encoded, err := encodeMockProto("aiserver.v1.GetUsableModelsResponse", payload)
|
||||
|
||||
@@ -950,6 +950,8 @@ func normalizeModelAdapterTestType(value string) string {
|
||||
|
||||
func normalizeModelAdapterTestReasoning(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "":
|
||||
return ""
|
||||
case "low", "medium", "high", "xhigh", "max":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
serverconfig "cursor/internal/backend/server/config"
|
||||
)
|
||||
|
||||
func TestNormalizeModelAdapterTestProviderReasoningPreservesBlank(t *testing.T) {
|
||||
adapter := serverconfig.ModelAdapterConfig{Type: "openai", ReasoningEffort: ""}
|
||||
|
||||
if got := normalizeModelAdapterTestProviderReasoning(adapter); got != "" {
|
||||
t.Fatalf("reasoning effort = %q, want blank", got)
|
||||
}
|
||||
}
|
||||
@@ -141,8 +141,8 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
|
||||
return nil, errors.New("模型适配器 tooltipData 不能为空")
|
||||
case next.ModelID == "":
|
||||
return nil, errors.New("模型适配器 modelID 不能为空")
|
||||
case next.Type == "openai" && next.ReasoningEffort == "":
|
||||
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh、max")
|
||||
case next.Type == "openai" && !isSupportedReasoningEffort(next.ReasoningEffort):
|
||||
return nil, errors.New("模型适配器 reasoningEffort 仅支持空值、low、medium、high、xhigh、max")
|
||||
case next.Type == "openai" && next.OpenAIEndpoint == "":
|
||||
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses 或 /v1/chat/completions")
|
||||
case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
|
||||
@@ -224,13 +224,15 @@ func validateHeadersJSON(value string) error {
|
||||
}
|
||||
|
||||
func normalizeReasoningEffort(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "medium":
|
||||
return "medium"
|
||||
case "low", "high", "xhigh", "max":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func isSupportedReasoningEffort(value string) bool {
|
||||
switch value {
|
||||
case "", "low", "medium", "high", "xhigh", "max":
|
||||
return true
|
||||
default:
|
||||
return ""
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package runtime
|
||||
|
||||
import "testing"
|
||||
|
||||
func testRuntimeModelAdapter(reasoningEffort string) ModelAdapterConfig {
|
||||
return ModelAdapterConfig{
|
||||
DisplayName: "non-reasoning-model",
|
||||
Type: "openai",
|
||||
BaseURL: "https://api.example.com/v1",
|
||||
APIKey: "test-key",
|
||||
TooltipData: "non-reasoning-model",
|
||||
ModelID: "non-reasoning-model",
|
||||
ReasoningEffort: reasoningEffort,
|
||||
OpenAIEndpoint: "/v1/responses",
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeModelAdapterConfigsAllowsBlankReasoningEffort(t *testing.T) {
|
||||
adapters, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{testRuntimeModelAdapter("")})
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizeModelAdapterConfigs returned error: %v", err)
|
||||
}
|
||||
if got := adapters[0].ReasoningEffort; got != "" {
|
||||
t.Fatalf("ReasoningEffort = %q, want blank", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeModelAdapterConfigsRejectsUnknownReasoningEffort(t *testing.T) {
|
||||
if _, err := NormalizeModelAdapterConfigs([]ModelAdapterConfig{testRuntimeModelAdapter("unsupported")}); err == nil {
|
||||
t.Fatal("NormalizeModelAdapterConfigs should reject an unknown reasoning effort")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user