返回 DeepSeek-Reasonix
default_live_test.go
根目录 / internal / websearch / default_live_test.go
1 //go:build live
2
3 package websearch_test
4
5 import (
6 "context"
7 "encoding/json"
8 "os"
9 "testing"
10 "time"
11
12 "reasonix/internal/boot"
13 "reasonix/internal/config"
14 "reasonix/internal/netclient"
15 "reasonix/internal/provider"
16 "reasonix/internal/tool"
17 "reasonix/internal/websearch"
18 )
19
20 // Explicitly opt in to two bounded main-model calls and one independent search.
21 // Only counts are logged; no credentials or conversation content are written.
22 func TestLiveDefaultDeepSeekSearchRoundTrip(t *testing.T) {
23 if os.Getenv("REASONIX_LIVE_WEB_SEARCH") != "1" {
24 t.Skip("set REASONIX_LIVE_WEB_SEARCH=1")
25 }
26 cfg := config.Default()
27 entry, ok := cfg.ResolveModel(cfg.DefaultModel)
28 if !ok {
29 t.Fatal("default provider did not resolve")
30 }
31 entry.ResolveAPIKeyFromProcessEnvForProbe()
32 if !entry.Configured() {
33 t.Skip("official account not configured")
34 }
35 if entry.Kind != "openai" {
36 t.Fatalf("default kind = %s", entry.Kind)
37 }
38 proxy := netclient.ProxySpec{Mode: netclient.ModeAuto}
39 p, err := boot.NewProviderWithProxy(entry, proxy)
40 if err != nil {
41 t.Fatal(err)
42 }
43 if c, ok := p.(interface{ CloseIdleConnections() }); ok {
44 defer c.CloseIdleConnections()
45 }
46 reg := tool.NewRegistry()
47 route := cfg.ResolveWebSearchProvider(entry)
48 if route == nil {
49 t.Fatal("no independent search route")
50 }
51 reg.Add(&websearch.Tool{Factory: func() (provider.Provider, error) {
52 return provider.New(route.Kind, provider.Config{Name: route.Name, BaseURL: route.BaseURL, Model: route.Model, APIKey: route.APIKey(), Extra: map[string]any{"web_search": true, "reject_redirects": true, "thinking": route.Thinking, "effort": route.Effort}})
53 }})
54 search, ok := reg.Get("web_search")
55 if !ok {
56 t.Fatal("missing independent search")
57 }
58 ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
59 defer cancel()
60 collect := func(req provider.Request) provider.Message {
61 t.Helper()
62 ch, err := p.Stream(ctx, req)
63 if err != nil {
64 t.Fatal(err)
65 }
66 m := provider.Message{Role: provider.RoleAssistant}
67 done := false
68 for c := range ch {
69 switch c.Type {
70 case provider.ChunkText:
71 m.Content += c.Text
72 case provider.ChunkReasoning:
73 m.ReasoningContent += c.Text
74 case provider.ChunkToolCall:
75 if c.ToolCall != nil {
76 m.ToolCalls = append(m.ToolCalls, *c.ToolCall)
77 }
78 case provider.ChunkError:
79 t.Fatalf("main request failed: %v", c.Err)
80 case provider.ChunkDone:
81 done = true
82 }
83 }
84 if !done {
85 t.Fatal("main request interrupted")
86 }
87 return m
88 }
89 messages := []provider.Message{{Role: provider.RoleUser, Content: "Call web_search exactly once for the official DeepSeek thinking-mode API documentation, then summarize its reasoning passback requirement in one sentence with a source link."}}
90 first := collect(provider.Request{Messages: messages, Tools: reg.Schemas(), MaxTokens: 2048})
91 if len(first.ToolCalls) != 1 || first.ToolCalls[0].Name != "web_search" {
92 t.Fatalf("expected one search call, got %d", len(first.ToolCalls))
93 }
94 call := first.ToolCalls[0]
95 output, err := search.Execute(ctx, json.RawMessage(call.Arguments))
96 if err != nil {
97 t.Fatal(err)
98 }
99 sources := provider.ParseServerSearchOutput(output)
100 if len(sources) == 0 {
101 t.Fatal("no structured search sources")
102 }
103 messages = append(messages, first, provider.Message{Role: provider.RoleTool, ToolCallID: call.ID, Name: call.Name, Content: output})
104 last := collect(provider.Request{Messages: messages, Tools: reg.Schemas(), MaxTokens: 2048})
105 if last.Content == "" || len(last.ToolCalls) > 0 {
106 t.Fatal("main model did not finish after search")
107 }
108 t.Logf("main_protocol=%s sources=%d first_reasoning_bytes=%d final_bytes=%d", entry.Kind, len(sources), len(first.ReasoningContent), len(last.Content))
109 }
110
110 lines GO