返回 DeepSeek-Reasonix
realdeepseek_test.go
根目录 / internal / provider / anthropic / realdeepseek_test.go
1 //go:build live
2
3 package anthropic
4
5 import (
6 "context"
7 "encoding/json"
8 "os"
9 "strings"
10 "testing"
11 "time"
12
13 "reasonix/internal/provider"
14 )
15
16 // TestRealDeepSeekAnthropicToolLoop exercises the official Messages endpoint's
17 // unsigned thinking replay contract. It is build-tagged and credential-gated so
18 // ordinary CI remains deterministic and free of live API cost.
19 func TestRealDeepSeekAnthropicToolLoop(t *testing.T) {
20 key := os.Getenv("DEEPSEEK_API_KEY")
21 if key == "" {
22 t.Skip("DEEPSEEK_API_KEY not set — skipping live probe")
23 }
24
25 p, err := New(provider.Config{
26 Name: "deepseek-anthropic",
27 BaseURL: "https://api.deepseek.com/anthropic",
28 Model: "deepseek-v4-flash",
29 APIKey: key,
30 Extra: map[string]any{
31 "api_key_env": "DEEPSEEK_API_KEY",
32 "thinking": "enabled",
33 "effort": "high",
34 },
35 })
36 if err != nil {
37 t.Fatalf("New: %v", err)
38 }
39
40 tools := []provider.ToolSchema{{
41 Name: "get_marker",
42 Description: "Return a fixed integration-test marker. Call this tool when the user asks for the marker.",
43 Parameters: json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`),
44 }}
45 messages := []provider.Message{
46 {Role: provider.RoleSystem, Content: "You are a concise tool-using assistant."},
47 {Role: provider.RoleUser, Content: "Use get_marker to obtain the integration-test marker, then tell me the result."},
48 }
49
50 first := collectLiveDeepSeekTurn(t, p, provider.Request{Messages: messages, Tools: tools, MaxTokens: 512})
51 if len(first.calls) == 0 {
52 t.Fatalf("first turn returned no tool call; text=%q reasoning_len=%d", first.text, len(first.reasoning))
53 }
54 if strings.TrimSpace(first.reasoning) == "" {
55 t.Fatal("first tool-call turn returned no reasoning to replay")
56 }
57
58 messages = append(messages,
59 provider.Message{
60 Role: provider.RoleAssistant,
61 Content: first.text,
62 ReasoningContent: first.reasoning,
63 ToolCalls: first.calls,
64 },
65 provider.Message{
66 Role: provider.RoleTool,
67 ToolCallID: first.calls[0].ID,
68 Name: first.calls[0].Name,
69 Content: "protocol-round-trip-ok",
70 },
71 )
72 second := collectLiveDeepSeekTurn(t, p, provider.Request{Messages: messages, Tools: tools, MaxTokens: 512})
73 if strings.TrimSpace(second.text) == "" {
74 t.Fatalf("second turn returned no visible answer; reasoning_len=%d calls=%d", len(second.reasoning), len(second.calls))
75 }
76 t.Logf("first: reasoning=%d calls=%d prompt=%d cache_hit=%d; second: text=%d reasoning=%d prompt=%d cache_hit=%d",
77 len(first.reasoning), len(first.calls), first.promptTokens, first.cacheHitTokens,
78 len(second.text), len(second.reasoning), second.promptTokens, second.cacheHitTokens)
79 }
80
81 type liveDeepSeekTurn struct {
82 text, reasoning string
83 calls []provider.ToolCall
84 promptTokens, cacheHitTokens int
85 }
86
87 func collectLiveDeepSeekTurn(t *testing.T, p provider.Provider, req provider.Request) liveDeepSeekTurn {
88 t.Helper()
89 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
90 defer cancel()
91 ch, err := p.Stream(ctx, req)
92 if err != nil {
93 t.Fatalf("Stream: %v", err)
94 }
95 var out liveDeepSeekTurn
96 var text, reasoning strings.Builder
97 for chunk := range ch {
98 switch chunk.Type {
99 case provider.ChunkText:
100 text.WriteString(chunk.Text)
101 case provider.ChunkReasoning:
102 reasoning.WriteString(chunk.Text)
103 case provider.ChunkToolCall:
104 if chunk.ToolCall != nil {
105 out.calls = append(out.calls, *chunk.ToolCall)
106 }
107 case provider.ChunkUsage:
108 if chunk.Usage != nil {
109 out.promptTokens = chunk.Usage.PromptTokens
110 out.cacheHitTokens = chunk.Usage.CacheHitTokens
111 }
112 case provider.ChunkError:
113 t.Fatalf("stream error: %v", chunk.Err)
114 }
115 }
116 out.text = text.String()
117 out.reasoning = reasoning.String()
118 return out
119 }
120
120 lines GO