fix(openai_compat): detect DeepSeek models behind proxies for reasoning_content replay
isDeepSeekReasoningProvider previously only detected DeepSeek via providerName == 'deepseek' or apiBase host == 'deepseek.com'. Users proxying DeepSeek models through third-party endpoints (e.g. opencode.ai) would not be detected, causing prepareMessagesForRequest to route into stripReasoningMessages, which unconditionally blanks all reasoning_content — including for tool-interaction turns where DeepSeek requires it. Fix: add model-based detection via isDeepSeekModel() which checks if the model name contains 'deepseek'. Also add prepareMessagesForRequest and isDeepSeekReasoningProvider to accept the model parameter.
This commit is contained in:
parent
6e1fab80e2
commit
45b665ab79
2 changed files with 141 additions and 5 deletions
|
|
@ -144,7 +144,7 @@ func (p *Provider) buildRequestBody(
|
|||
|
||||
requestBody := map[string]any{
|
||||
"model": model,
|
||||
"messages": common.SerializeMessages(p.prepareMessagesForRequest(messages)),
|
||||
"messages": common.SerializeMessages(p.prepareMessagesForRequest(messages, model)),
|
||||
}
|
||||
|
||||
// When fallback uses a different provider (e.g. DeepSeek), that provider must not inject web_search_preview.
|
||||
|
|
@ -208,19 +208,19 @@ func (p *Provider) SetProviderName(providerName string) {
|
|||
p.providerName = strings.ToLower(strings.TrimSpace(providerName))
|
||||
}
|
||||
|
||||
func (p *Provider) prepareMessagesForRequest(messages []Message) []Message {
|
||||
func (p *Provider) prepareMessagesForRequest(messages []Message, model string) []Message {
|
||||
if len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.isDeepSeekReasoningProvider() {
|
||||
if p.isDeepSeekReasoningProvider(model) {
|
||||
return filterDeepSeekReasoningMessages(messages)
|
||||
}
|
||||
return stripReasoningMessages(messages)
|
||||
}
|
||||
|
||||
func (p *Provider) isDeepSeekReasoningProvider() bool {
|
||||
return p.providerName == "deepseek" || isDeepSeekHost(p.apiBase)
|
||||
func (p *Provider) isDeepSeekReasoningProvider(model string) bool {
|
||||
return p.providerName == "deepseek" || isDeepSeekHost(p.apiBase) || isDeepSeekModel(model)
|
||||
}
|
||||
|
||||
func isDeepSeekHost(apiBase string) bool {
|
||||
|
|
@ -232,6 +232,10 @@ func isDeepSeekHost(apiBase string) bool {
|
|||
return host == "deepseek.com" || strings.HasSuffix(host, ".deepseek.com")
|
||||
}
|
||||
|
||||
func isDeepSeekModel(model string) bool {
|
||||
return strings.Contains(strings.ToLower(model), "deepseek")
|
||||
}
|
||||
|
||||
func filterDeepSeekReasoningMessages(messages []Message) []Message {
|
||||
out := make([]Message, 0, len(messages))
|
||||
start := 0
|
||||
|
|
|
|||
|
|
@ -632,6 +632,138 @@ func TestProviderChat_DeepSeekDocsReplayRequirements(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProviderChat_DeepSeekModelViaProxyPreservesReasoningForToolTurns(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
|
||||
}
|
||||
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, "")
|
||||
// Provider is "openai" (not "deepseek"), host is localhost (not deepseek.com),
|
||||
// but model name contains "deepseek" — we should still route to the DeepSeek
|
||||
// reasoning-content filter.
|
||||
p.SetProviderName("openai")
|
||||
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: "get date"},
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "",
|
||||
ReasoningContent: "I need tomorrow's date before checking the weather.",
|
||||
ToolCalls: []ToolCall{
|
||||
{ID: "call_date", Name: "get_date", Arguments: map[string]any{}},
|
||||
},
|
||||
},
|
||||
{Role: "tool", ToolCallID: "call_date", Content: "2026-04-29"},
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "Tomorrow is 2026-04-30.",
|
||||
ReasoningContent: "Now I can share the weather.",
|
||||
},
|
||||
{Role: "user", Content: "What about Guangzhou?"},
|
||||
}
|
||||
|
||||
_, err := p.Chat(t.Context(), messages, nil, "deepseek-v4-flash", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error = %v", err)
|
||||
}
|
||||
|
||||
reqMessages, ok := requestBody["messages"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("messages is not []any: %T", requestBody["messages"])
|
||||
}
|
||||
|
||||
// Tool-interaction assistant (index 1) must preserve reasoning_content.
|
||||
toolAssistant, ok := reqMessages[1].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("tool assistant message is not map[string]any: %T", reqMessages[1])
|
||||
}
|
||||
if toolAssistant["reasoning_content"] != "I need tomorrow's date before checking the weather." {
|
||||
t.Fatalf(
|
||||
"tool assistant reasoning_content = %v, want preserved for DeepSeek model via proxy",
|
||||
toolAssistant["reasoning_content"],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDeepSeekReasoningProvider_ModelDetection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
providerName string
|
||||
apiBase string
|
||||
model string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "provider name is deepseek",
|
||||
providerName: "deepseek",
|
||||
apiBase: "https://api.openai.com/v1",
|
||||
model: "gpt-5.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "api base is deepseek.com",
|
||||
providerName: "openai",
|
||||
apiBase: "https://api.deepseek.com/v1",
|
||||
model: "gpt-5.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "model contains deepseek via proxy",
|
||||
providerName: "openai",
|
||||
apiBase: "https://opencode.ai/zen/go/v1",
|
||||
model: "deepseek-v4-flash",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "model contains deepseek-chat",
|
||||
providerName: "openai",
|
||||
apiBase: "https://api.openai.com/v1",
|
||||
model: "deepseek-chat",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no deepseek anywhere",
|
||||
providerName: "openai",
|
||||
apiBase: "https://api.openai.com/v1",
|
||||
model: "gpt-5.4",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := NewProvider("key", tt.apiBase, "")
|
||||
p.SetProviderName(tt.providerName)
|
||||
got := p.isDeepSeekReasoningProvider(tt.model)
|
||||
if got != tt.want {
|
||||
t.Fatalf("isDeepSeekReasoningProvider(%q) = %v, want %v", tt.model, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue