From cd408067fcd08ba03d79f0871290cba0c4ed23ab Mon Sep 17 00:00:00 2001 From: Alix-007 <267018309+Alix-007@users.noreply.github.com> Date: Thu, 26 Mar 2026 03:10:17 +0800 Subject: [PATCH 1/2] fix(openai_compat): omit reasoning_content for Mistral requests --- pkg/providers/openai_compat/provider.go | 24 +++++- pkg/providers/openai_compat/provider_test.go | 86 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 90bc683b8..306861882 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -100,10 +100,11 @@ func (p *Provider) buildRequestBody( messages []Message, tools []ToolDefinition, model string, options map[string]any, ) map[string]any { model = normalizeModel(model, p.apiBase) + serializedMessages := common.SerializeMessages(messagesForRequest(messages, p.apiBase)) requestBody := map[string]any{ "model": model, - "messages": common.SerializeMessages(messages), + "messages": serializedMessages, } // When fallback uses a different provider (e.g. DeepSeek), that provider must not inject web_search_preview. @@ -156,6 +157,27 @@ func (p *Provider) buildRequestBody( return requestBody } +func messagesForRequest(messages []Message, apiBase string) []Message { + if !isReasoningContentUnsupportedHost(apiBase) { + return messages + } + out := make([]Message, len(messages)) + copy(out, messages) + for i := range out { + out[i].ReasoningContent = "" + } + return out +} + +func isReasoningContentUnsupportedHost(apiBase string) bool { + u, err := url.Parse(apiBase) + if err != nil { + return false + } + host := strings.ToLower(u.Hostname()) + return host == "api.mistral.ai" || strings.HasSuffix(host, ".mistral.ai") +} + func (p *Provider) Chat( ctx context.Context, messages []Message, diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index ab632ccf3..18e3ac2a8 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -252,6 +252,73 @@ func TestProviderChat_PreservesReasoningContentInHistory(t *testing.T) { } } +func TestProviderChat_OmitsReasoningContentForMistral(t *testing.T) { + var requestBody map[string]any + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + reqMessages, _ := requestBody["messages"].([]any) + for _, rawMsg := range reqMessages { + msg, ok := rawMsg.(map[string]any) + if !ok { + continue + } + if _, exists := msg["reasoning_content"]; exists { + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"object":"error","message":"Extra inputs are not permitted","type":"invalid_request_error"}`)) + return + } + } + + resp := map[string]any{ + "choices": []map[string]any{ + { + "message": map[string]any{"content": "ok"}, + "finish_reason": "stop", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + p := NewProvider("key", server.URL, "") + p.apiBase = "https://api.mistral.ai/v1" + p.httpClient = &http.Client{ + Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { + r.URL, _ = url.Parse(server.URL + r.URL.Path) + return http.DefaultTransport.RoundTrip(r) + }), + } + + messages := []Message{ + {Role: "user", Content: "What is 1+1?"}, + {Role: "assistant", Content: "2", ReasoningContent: "Let me think... 1+1=2"}, + {Role: "user", Content: "What about 2+2?"}, + } + + if _, err := p.Chat(t.Context(), messages, nil, "mistral-small-latest", nil); err != nil { + t.Fatalf("Chat() error = %v", err) + } + + reqMessages, ok := requestBody["messages"].([]any) + if !ok { + t.Fatalf("messages is not []any: %T", requestBody["messages"]) + } + assistantMsg, ok := reqMessages[1].(map[string]any) + if !ok { + t.Fatalf("assistant message is not map[string]any: %T", reqMessages[1]) + } + if _, exists := assistantMsg["reasoning_content"]; exists { + t.Fatal("reasoning_content should be omitted for Mistral requests") + } +} + func TestProviderChat_HTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad request", http.StatusBadRequest) @@ -929,6 +996,25 @@ func TestSupportsPromptCacheKey(t *testing.T) { } } +func TestIsReasoningContentUnsupportedHost(t *testing.T) { + tests := []struct { + apiBase string + want bool + }{ + {"https://api.mistral.ai/v1", true}, + {"https://edge.mistral.ai/v1", true}, + {"https://api.openai.com/v1", false}, + {"https://api.deepseek.com/v1", false}, + {"", false}, + {"not-a-url", false}, + } + for _, tt := range tests { + if got := isReasoningContentUnsupportedHost(tt.apiBase); got != tt.want { + t.Errorf("isReasoningContentUnsupportedHost(%q) = %v, want %v", tt.apiBase, got, tt.want) + } + } +} + func TestBuildToolsList_NativeSearchAddsWebSearchPreview(t *testing.T) { tools := []ToolDefinition{ {Type: "function", Function: ToolFunctionDefinition{Name: "read_file", Description: "read"}}, From 77b0b0d6861ac13e7b2d0a1827f0b3f4b7e84208 Mon Sep 17 00:00:00 2001 From: Alix-007 <267018309+Alix-007@users.noreply.github.com> Date: Thu, 26 Mar 2026 03:23:13 +0800 Subject: [PATCH 2/2] test(openai_compat): wrap long JSON literal for lint formatting --- pkg/providers/openai_compat/provider_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 18e3ac2a8..3e038d279 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -269,7 +269,9 @@ func TestProviderChat_OmitsReasoningContentForMistral(t *testing.T) { } if _, exists := msg["reasoning_content"]; exists { w.WriteHeader(http.StatusUnprocessableEntity) - _, _ = w.Write([]byte(`{"object":"error","message":"Extra inputs are not permitted","type":"invalid_request_error"}`)) + _, _ = w.Write([]byte( + `{"object":"error","message":"Extra inputs are not permitted","type":"invalid_request_error"}`, + )) return } }