package vision import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "sync" "sync/atomic" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // writeFileBytes 写一个测试用的字节文件。 func writeFileBytes(path string, data []byte) error { if err := os.WriteFile(path, data, 0o600); err != nil { return fmt.Errorf("write test file: %w", err) } return nil } // writeFileBytesAll 在 dir 下写 name(含父目录),用于摆放测试用的上传产物。 func writeFileBytesAll(dir, name string, data []byte) error { if err := os.MkdirAll(dir, 0o700); err != nil { return fmt.Errorf("create test dir: %w", err) } return writeFileBytes(filepath.Join(dir, name), data) } // imageURLRecorder 是一个 /chat/completions endpoint,记录发往上游的 // image_url 取值,使测试能断言「服务端真的把图取回并内联了」而不是 // 原样透传 URL。 type imageURLRecorder struct { mu sync.Mutex calls int urls []string } func (r *imageURLRecorder) seen() (int, []string) { r.mu.Lock() defer r.mu.Unlock() return r.calls, append([]string(nil), r.urls...) } // newImageURLRecorderServer 起一个上游 endpoint,把每次请求里的 // image_url.url 记下来。 func newImageURLRecorderServer(t *testing.T) (*httptest.Server, *imageURLRecorder) { t.Helper() rec := &imageURLRecorder{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body struct { Messages []struct { Content []struct { ImageURL *struct { URL string `json:"url"` } `json:"image_url"` } `json:"content"` } `json:"messages"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Errorf("decode upstream body: %v", err) } rec.mu.Lock() rec.calls++ for _, m := range body.Messages { for _, part := range m.Content { if part.ImageURL != nil { rec.urls = append(rec.urls, part.ImageURL.URL) } } } rec.mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, `{"choices":[{"message":{"content":"a small square"}}]}`) })) t.Cleanup(srv.Close) return srv, rec } // newImageServer 起一个提供图像字节的来源服务,contentType 为 "" 时 // 刻意不发 Content-Type(考验字节嗅探)。 func newImageServer(t *testing.T, data []byte, contentType string, hits *atomic.Int32) *httptest.Server { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if hits != nil { hits.Add(1) } if contentType == "" { w.Header()["Content-Type"] = nil w.Header().Set("Content-Type", "") } else { w.Header().Set("Content-Type", contentType) } _, _ = w.Write(data) })) t.Cleanup(srv.Close) return srv } // newAdapter 构造一个指向 up 的适配器,opts 追加在必需项之后。 func newAdapter(up string, opts ...OpenAIOption) *OpenAICompatibleAdapter { return NewOpenAICompatible(config.LLMConfig{BaseURL: up, Model: "vision-test"}, opts...) }