Merge pull request #1 from afjcjsbx/codex/resolve-main-upstream-merge
Merge upstream/main into main
This commit is contained in:
commit
e95bcaf3e3
6 changed files with 255 additions and 2 deletions
|
|
@ -605,6 +605,43 @@ func TestProcessMessage_PassesExplicitThinkingOffToProviderWithoutThinkingCapabi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_PassesDeepSeekThinkingLevelToThinkingCapableProvider(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: t.TempDir(),
|
||||||
|
ModelName: "deepseek-v4-flash",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ModelList: []*config.ModelConfig{{
|
||||||
|
ModelName: "deepseek-v4-flash",
|
||||||
|
Provider: "deepseek",
|
||||||
|
Model: "deepseek-v4-flash",
|
||||||
|
ThinkingLevel: "xhigh",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := &thinkingRecordingProvider{}
|
||||||
|
al := NewAgentLoop(cfg, bus.NewMessageBus(), provider)
|
||||||
|
|
||||||
|
response, err := al.processMessage(context.Background(), testInboundMessage(bus.InboundMessage{
|
||||||
|
Channel: "pico",
|
||||||
|
ChatID: "chat-1",
|
||||||
|
Content: "hello",
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("processMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
if response != "Mock response" {
|
||||||
|
t.Fatalf("processMessage() response = %q, want %q", response, "Mock response")
|
||||||
|
}
|
||||||
|
if got := provider.lastOptions["thinking_level"]; got != "xhigh" {
|
||||||
|
t.Fatalf("thinking_level option = %#v, want %q", got, "xhigh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProcessMessage_SuppressesReasoningWhenThinkingOff(t *testing.T) {
|
func TestProcessMessage_SuppressesReasoningWhenThinkingOff(t *testing.T) {
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Agents: config.AgentsConfig{
|
Agents: config.AgentsConfig{
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,14 @@ func (p *Pipeline) SetupTurn(ctx context.Context, ts *turnState) (*turnExecution
|
||||||
history, messages, fit = trimHistoryToFitContextWindow(
|
history, messages, fit = trimHistoryToFitContextWindow(
|
||||||
history,
|
history,
|
||||||
func(trimmedHistory []providers.Message) []providers.Message {
|
func(trimmedHistory []providers.Message) []providers.Message {
|
||||||
rebuildPromptReq := promptBuildRequestForTurn(ts, trimmedHistory, summary, ts.userMessage, ts.media, cfg)
|
rebuildPromptReq := promptBuildRequestForTurn(
|
||||||
|
ts,
|
||||||
|
trimmedHistory,
|
||||||
|
summary,
|
||||||
|
ts.userMessage,
|
||||||
|
ts.media,
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
rebuildPromptReq.ActiveSkills = append([]string(nil), contextualSkills...)
|
rebuildPromptReq.ActiveSkills = append([]string(nil), contextualSkills...)
|
||||||
rebuilt := ts.agent.ContextBuilder.BuildMessagesFromPrompt(rebuildPromptReq)
|
rebuilt := ts.agent.ContextBuilder.BuildMessagesFromPrompt(rebuildPromptReq)
|
||||||
return resolveMediaRefs(rebuilt, p.MediaStore, maxMediaSize)
|
return resolveMediaRefs(rebuilt, p.MediaStore, maxMediaSize)
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,30 @@ func TestCreateProviderFromConfig_UsesExplicitProvider(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateProviderFromConfig_DeepSeekSupportsThinking(t *testing.T) {
|
||||||
|
cfg := &config.ModelConfig{
|
||||||
|
ModelName: "deepseek-v4-flash",
|
||||||
|
Provider: "deepseek",
|
||||||
|
Model: "deepseek-v4-flash",
|
||||||
|
}
|
||||||
|
cfg.SetAPIKey("test-key")
|
||||||
|
|
||||||
|
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if modelID != "deepseek-v4-flash" {
|
||||||
|
t.Fatalf("modelID = %q, want %q", modelID, "deepseek-v4-flash")
|
||||||
|
}
|
||||||
|
tc, ok := provider.(ThinkingCapable)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("provider %T should implement ThinkingCapable for DeepSeek", provider)
|
||||||
|
}
|
||||||
|
if !tc.SupportsThinking() {
|
||||||
|
t.Fatalf("DeepSeek provider SupportsThinking() = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateProviderFromConfig_PreservesExplicitProviderPrefixedModel(t *testing.T) {
|
func TestCreateProviderFromConfig_PreservesExplicitProviderPrefixedModel(t *testing.T) {
|
||||||
cfg := &config.ModelConfig{
|
cfg := &config.ModelConfig{
|
||||||
ModelName: "test-openai",
|
ModelName: "test-openai",
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,13 @@ func (p *HTTPProvider) SupportsNativeSearch() bool {
|
||||||
return p.delegate.SupportsNativeSearch()
|
return p.delegate.SupportsNativeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *HTTPProvider) SupportsThinking() bool {
|
||||||
|
if p == nil || p.delegate == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return p.delegate.SupportsThinking()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *HTTPProvider) SetProviderName(providerName string) {
|
func (p *HTTPProvider) SetProviderName(providerName string) {
|
||||||
if p == nil || p.delegate == nil {
|
if p == nil || p.delegate == nil {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/common"
|
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/messageutil"
|
"github.com/sipeed/picoclaw/pkg/providers/messageutil"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
|
|
@ -204,7 +205,16 @@ func (p *Provider) buildRequestBody(
|
||||||
|
|
||||||
func (p *Provider) applyThinkingControl(requestBody map[string]any, model string, options map[string]any) {
|
func (p *Provider) applyThinkingControl(requestBody map[string]any, model string, options map[string]any) {
|
||||||
level, ok := normalizedThinkingLevel(options)
|
level, ok := normalizedThinkingLevel(options)
|
||||||
if !ok || level != "off" {
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.SupportsThinking() {
|
||||||
|
p.applyDeepSeekThinkingControl(requestBody, level)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if level != "off" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,6 +226,28 @@ func (p *Provider) applyThinkingControl(requestBody map[string]any, model string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) applyDeepSeekThinkingControl(requestBody map[string]any, level string) {
|
||||||
|
switch level {
|
||||||
|
case "off":
|
||||||
|
requestBody["thinking"] = map[string]any{"type": "disabled"}
|
||||||
|
case "low", "medium", "high":
|
||||||
|
requestBody["thinking"] = map[string]any{"type": "enabled"}
|
||||||
|
requestBody["reasoning_effort"] = "high"
|
||||||
|
case "xhigh":
|
||||||
|
requestBody["thinking"] = map[string]any{"type": "enabled"}
|
||||||
|
requestBody["reasoning_effort"] = "max"
|
||||||
|
case "adaptive":
|
||||||
|
logger.WarnCF("provider.openai_compat",
|
||||||
|
`DeepSeek does not support thinking_level="adaptive"; using provider default thinking behavior`,
|
||||||
|
map[string]any{
|
||||||
|
"provider": p.providerName,
|
||||||
|
"api_base": p.apiBase,
|
||||||
|
"thinking_level": level,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizedThinkingLevel(options map[string]any) (string, bool) {
|
func normalizedThinkingLevel(options map[string]any) (string, bool) {
|
||||||
raw, ok := options["thinking_level"].(string)
|
raw, ok := options["thinking_level"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -290,6 +322,10 @@ func (p *Provider) SetProviderName(providerName string) {
|
||||||
p.providerName = strings.ToLower(strings.TrimSpace(providerName))
|
p.providerName = strings.ToLower(strings.TrimSpace(providerName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) SupportsThinking() bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(p.providerName), "deepseek") || isDeepSeekHost(p.apiBase)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) prepareMessagesForRequest(messages []Message) []Message {
|
func (p *Provider) prepareMessagesForRequest(messages []Message) []Message {
|
||||||
if len(messages) == 0 {
|
if len(messages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/common"
|
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
@ -125,6 +127,146 @@ func TestBuildRequestBody_PreservesDoubaoRequestWhenThinkingLevelIsNotOff(t *tes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildRequestBody_MapsDeepSeekThinkingLevels(t *testing.T) {
|
||||||
|
p := NewProvider("key", "https://api.deepseek.com/v1", "")
|
||||||
|
p.SetProviderName("deepseek")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
level string
|
||||||
|
wantThinkingType string
|
||||||
|
wantEffort any
|
||||||
|
}{
|
||||||
|
{name: "off", level: "off", wantThinkingType: "disabled"},
|
||||||
|
{name: "low", level: "low", wantThinkingType: "enabled", wantEffort: "high"},
|
||||||
|
{name: "medium", level: "medium", wantThinkingType: "enabled", wantEffort: "high"},
|
||||||
|
{name: "high", level: "high", wantThinkingType: "enabled", wantEffort: "high"},
|
||||||
|
{name: "xhigh", level: "xhigh", wantThinkingType: "enabled", wantEffort: "max"},
|
||||||
|
{name: "adaptive", level: "adaptive"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
body := p.buildRequestBody(
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
"deepseek-v4-pro",
|
||||||
|
map[string]any{"thinking_level": tt.level},
|
||||||
|
)
|
||||||
|
|
||||||
|
if tt.wantThinkingType == "" {
|
||||||
|
if _, ok := body["thinking"]; ok {
|
||||||
|
t.Fatalf("thinking should be omitted for %q, got %#v", tt.level, body["thinking"])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thinking, ok := body["thinking"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("thinking = %#v, want map", body["thinking"])
|
||||||
|
}
|
||||||
|
if got := thinking["type"]; got != tt.wantThinkingType {
|
||||||
|
t.Fatalf("thinking.type = %#v, want %q", got, tt.wantThinkingType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantEffort == nil {
|
||||||
|
if _, ok := body["reasoning_effort"]; ok {
|
||||||
|
t.Fatalf("reasoning_effort should be omitted for %q, got %#v", tt.level, body["reasoning_effort"])
|
||||||
|
}
|
||||||
|
} else if got := body["reasoning_effort"]; got != tt.wantEffort {
|
||||||
|
t.Fatalf("reasoning_effort = %#v, want %#v", got, tt.wantEffort)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildRequestBody_MapsDeepSeekThinkingLevelsByHost(t *testing.T) {
|
||||||
|
p := NewProvider("key", "https://api.deepseek.com/v1", "")
|
||||||
|
|
||||||
|
body := p.buildRequestBody(
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
"deepseek-v4-flash",
|
||||||
|
map[string]any{"thinking_level": "xhigh"},
|
||||||
|
)
|
||||||
|
|
||||||
|
thinking, ok := body["thinking"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("thinking = %#v, want map", body["thinking"])
|
||||||
|
}
|
||||||
|
if got := thinking["type"]; got != "enabled" {
|
||||||
|
t.Fatalf("thinking.type = %#v, want enabled", got)
|
||||||
|
}
|
||||||
|
if got := body["reasoning_effort"]; got != "max" {
|
||||||
|
t.Fatalf("reasoning_effort = %#v, want max", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildRequestBody_DeepSeekExtraBodyStillOverridesThinkingFields(t *testing.T) {
|
||||||
|
extraBody := map[string]any{
|
||||||
|
"thinking": map[string]any{"type": "disabled"},
|
||||||
|
"reasoning_effort": "max",
|
||||||
|
}
|
||||||
|
p := NewProvider("key", "https://api.deepseek.com/v1", "", WithExtraBody(extraBody))
|
||||||
|
p.SetProviderName("deepseek")
|
||||||
|
|
||||||
|
body := p.buildRequestBody(
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
"deepseek-v4-pro",
|
||||||
|
map[string]any{"thinking_level": "high"},
|
||||||
|
)
|
||||||
|
|
||||||
|
thinking, ok := body["thinking"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("thinking = %#v, want map", body["thinking"])
|
||||||
|
}
|
||||||
|
if got := thinking["type"]; got != "disabled" {
|
||||||
|
t.Fatalf("thinking.type = %#v, want disabled from extra_body override", got)
|
||||||
|
}
|
||||||
|
if got := body["reasoning_effort"]; got != "max" {
|
||||||
|
t.Fatalf("reasoning_effort = %#v, want max from extra_body override", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildRequestBody_WarnsForUnsupportedDeepSeekAdaptiveThinkingLevel(t *testing.T) {
|
||||||
|
logFile := t.TempDir() + "/deepseek-adaptive-warning.log"
|
||||||
|
prevLevel := logger.GetLevel()
|
||||||
|
logger.SetLevel(logger.WARN)
|
||||||
|
if err := logger.EnableFileLogging(logFile); err != nil {
|
||||||
|
t.Fatalf("EnableFileLogging() error = %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
logger.DisableFileLogging()
|
||||||
|
logger.SetLevel(prevLevel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
p := NewProvider("key", "https://api.deepseek.com/v1", "")
|
||||||
|
p.SetProviderName("deepseek")
|
||||||
|
|
||||||
|
body := p.buildRequestBody(
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
"deepseek-v4-pro",
|
||||||
|
map[string]any{"thinking_level": "adaptive"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, ok := body["thinking"]; ok {
|
||||||
|
t.Fatalf("thinking should be omitted for adaptive, got %#v", body["thinking"])
|
||||||
|
}
|
||||||
|
if _, ok := body["reasoning_effort"]; ok {
|
||||||
|
t.Fatalf("reasoning_effort should be omitted for adaptive, got %#v", body["reasoning_effort"])
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(logFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%q) error = %v", logFile, err)
|
||||||
|
}
|
||||||
|
logs := string(data)
|
||||||
|
if !strings.Contains(logs, `thinking_level=\"adaptive\"`) {
|
||||||
|
t.Fatalf("warning log = %q, want adaptive warning message", logs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProviderChat_ParsesToolCalls(t *testing.T) {
|
func TestProviderChat_ParsesToolCalls(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) {
|
||||||
resp := map[string]any{
|
resp := map[string]any{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue