返回 DeepSeek-Reasonix
live_test.go
根目录 / internal / imageinput / live_test.go
1 //go:build live
2
3 package imageinput_test
4
5 import (
6 "bytes"
7 "context"
8 "crypto/rand"
9 "encoding/base64"
10 "image"
11 "image/color"
12 "image/draw"
13 "image/png"
14 "os"
15 "strings"
16 "testing"
17 "time"
18
19 "golang.org/x/image/font"
20 "golang.org/x/image/font/basicfont"
21 "golang.org/x/image/math/fixed"
22 "reasonix/internal/config"
23 "reasonix/internal/event"
24 "reasonix/internal/imageinput"
25 "reasonix/internal/provider"
26 "reasonix/internal/provider/anthropic"
27 "reasonix/internal/provider/openai"
28 "reasonix/internal/provider/responses"
29 )
30
31 func fixture(t *testing.T) (string, string) {
32 t.Helper()
33 var b [4]byte
34 if _, err := rand.Read(b[:]); err != nil {
35 t.Fatal(err)
36 }
37 code := ""
38 for _, v := range b {
39 code += string("KRTXYZ"[int(v)%6])
40 }
41 im := image.NewRGBA(image.Rect(0, 0, 400, 160))
42 draw.Draw(im, im.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
43 draw.Draw(im, image.Rect(10, 70, 180, 150), image.NewUniform(color.RGBA{255, 0, 0, 255}), image.Point{}, draw.Src)
44 draw.Draw(im, image.Rect(220, 70, 390, 150), image.NewUniform(color.RGBA{0, 0, 255, 255}), image.Point{}, draw.Src)
45 // Scale a small bitmap font so random text is legible without external assets.
46 label := image.NewRGBA(image.Rect(0, 0, 80, 20))
47 draw.Draw(label, label.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
48 d := font.Drawer{Dst: label, Src: image.NewUniform(color.Black), Face: basicfont.Face7x13, Dot: fixed.P(2, 14)}
49 d.DrawString(strings.Join(strings.Split(code, ""), " "))
50 for y := range 60 {
51 for x := range 240 {
52 im.Set(60+x, y, label.At(x/3, y/3))
53 }
54 }
55 var out bytes.Buffer
56 if err := png.Encode(&out, im); err != nil {
57 t.Fatal(err)
58 }
59 return "data:image/png;base64," + base64.StdEncoding.EncodeToString(out.Bytes()), code
60 }
61 func collect(t *testing.T, p provider.Provider, r provider.Request) string {
62 t.Helper()
63 ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
64 defer cancel()
65 ch, err := p.Stream(ctx, r)
66 if err != nil {
67 t.Fatalf("request failed (%T); no request or credentials logged", err)
68 }
69 var out strings.Builder
70 for {
71 select {
72 case <-ctx.Done():
73 t.Fatal("request timed out")
74 case c, ok := <-ch:
75 if !ok {
76 return out.String()
77 }
78 if c.Err != nil {
79 t.Fatalf("stream failed (%T); no payload logged", c.Err)
80 }
81 if c.Type == provider.ChunkText {
82 out.WriteString(c.Text)
83 }
84 }
85 }
86 }
87 func TestLiveToolImages(t *testing.T) {
88 if os.Getenv("REASONIX_LIVE_TOOL_IMAGES") != "1" {
89 t.Skip("live probe disabled")
90 }
91 cfg, err := config.LoadForRootReadOnly("../..")
92 if err != nil {
93 t.Fatal("configuration unavailable")
94 }
95 key := ""
96 for _, p := range cfg.Providers {
97 if openai.IsDeepSeek(p.BaseURL) && p.APIKey() != "" {
98 key = p.APIKey()
99 break
100 }
101 }
102 if key == "" {
103 t.Skip("official DeepSeek credentials unavailable")
104 }
105 makeProvider := func(protocol, model string) provider.Provider {
106 cfg := provider.Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: model, APIKey: key, Extra: map[string]any{"thinking": "disabled"}}
107 if protocol == "responses" {
108 return responses.New(responses.Config{Name: "deepseek", BaseURL: cfg.BaseURL, Model: model, APIKey: key, Mode: "stateless", MaxOutputTokens: 2048, Extra: cfg.Extra})
109 }
110 var p provider.Provider
111 var e error
112 if protocol == "anthropic" {
113 cfg.BaseURL += "/anthropic"
114 p, e = anthropic.New(cfg)
115 } else {
116 p, e = openai.New(cfg)
117 }
118 if e != nil {
119 t.Fatal("provider construction failed")
120 }
121 return p
122 }
123 for _, protocol := range []string{"chat", "responses", "anthropic"} {
124 t.Run(protocol, func(t *testing.T) {
125 ref, code := fixture(t)
126 p := makeProvider(protocol, openai.OfficialDeepSeekVisionModel)
127 r := provider.Request{MaxTokens: 2048, Messages: []provider.Message{
128 {Role: provider.RoleUser, Content: "Return JSON with fields code (the printed letters and digits at the top of the image), left_color, right_color. Do not omit code. If unreadable say unreadable."},
129 {Role: provider.RoleAssistant, ReasoningContent: "I will inspect the image using the tool.", ToolCalls: []provider.ToolCall{{ID: "image1", Name: "view_image", Arguments: `{"path":"fixture.png"}`}}},
130 {Role: provider.RoleTool, ToolCallID: "image1", Name: "view_image", Content: "image loaded", Images: []string{ref}},
131 }}
132 out := collect(t, p, r)
133 if !strings.Contains(strings.ReplaceAll(strings.ToUpper(out), " ", ""), code) || !strings.Contains(strings.ToLower(out), "red") || !strings.Contains(strings.ToLower(out), "blue") {
134 t.Fatalf("image semantic verification failed: %s", out)
135 }
136 })
137 }
138 vp := makeProvider("chat", openai.OfficialDeepSeekVisionModel)
139 for _, mode := range []string{"explicit", "auto"} {
140 t.Run(mode, func(t *testing.T) {
141 target := "deepseek/" + openai.OfficialDeepSeekVisionModel
142 selected := 0
143 c := imageinput.Config{Model: target, Resolve: func(string) (provider.Provider, error) { return vp, nil }, Select: func(current, mode string) (string, bool) { selected++; return target, true }}
144 if mode == "auto" {
145 c.Model = "auto"
146 }
147 svc := imageinput.New(c)
148 for range 2 {
149 ref, code := fixture(t)
150 ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
151 summary, e := svc.Understand(ctx, "deepseek/deepseek-v4-flash", []string{ref}, nil, event.Discard)
152 cancel()
153 if e != nil {
154 t.Fatalf("summary failed (%T)", e)
155 }
156 text := collect(t, makeProvider("chat", "deepseek-v4-flash"), provider.Request{MaxTokens: 1024, Messages: []provider.Message{{Role: provider.RoleUser, Content: imageinput.AppendSummary("Return JSON with code, left_color, right_color from the supplied summary. Do not omit code.", summary)}}})
157 if !strings.Contains(strings.ReplaceAll(strings.ToUpper(text), " ", ""), code) {
158 t.Fatalf("summary relay failed: summary=%s answer=%s", summary.Summary, text)
159 }
160 }
161 if mode == "auto" && selected != 2 {
162 t.Fatal("auto selector was not used")
163 }
164 })
165 }
166 }
167
167 lines GO