fix(openai_compat): omit reasoning_content for Mistral requests
This commit is contained in:
parent
ed618e14aa
commit
cd408067fc
2 changed files with 109 additions and 1 deletions
|
|
@ -100,10 +100,11 @@ func (p *Provider) buildRequestBody(
|
||||||
messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||||
) map[string]any {
|
) map[string]any {
|
||||||
model = normalizeModel(model, p.apiBase)
|
model = normalizeModel(model, p.apiBase)
|
||||||
|
serializedMessages := common.SerializeMessages(messagesForRequest(messages, p.apiBase))
|
||||||
|
|
||||||
requestBody := map[string]any{
|
requestBody := map[string]any{
|
||||||
"model": model,
|
"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.
|
// 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
|
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(
|
func (p *Provider) Chat(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
messages []Message,
|
messages []Message,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestProviderChat_HTTPError(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
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) {
|
func TestBuildToolsList_NativeSearchAddsWebSearchPreview(t *testing.T) {
|
||||||
tools := []ToolDefinition{
|
tools := []ToolDefinition{
|
||||||
{Type: "function", Function: ToolFunctionDefinition{Name: "read_file", Description: "read"}},
|
{Type: "function", Function: ToolFunctionDefinition{Name: "read_file", Description: "read"}},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue