package vision import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "sync" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // modelRecorder is a /chat/completions endpoint that records the requested model, // so a test can pin which model each direction (Describe / Extract) asked for. type modelRecorder struct { mu sync.Mutex models []string } func (r *modelRecorder) seen() []string { r.mu.Lock() defer r.mu.Unlock() return append([]string(nil), r.models...) } func newModelRecorderServer(t *testing.T) (*httptest.Server, *modelRecorder) { t.Helper() rec := &modelRecorder{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body struct { Model string `json:"model"` } _ = json.NewDecoder(r.Body).Decode(&body) rec.mu.Lock() rec.models = append(rec.models, body.Model) rec.mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, `{"choices":[{"message":{"content":"ok"}}]}`) })) t.Cleanup(srv.Close) return srv, rec } // TestOpenAICompatibleAdapter_OCRUsesOwnModel pins the independent OCR model // (裁决 14.4.1 的「独立视觉模型 + OCR 模型」): when LLM_OCR_MODEL is set the OCR // direction sends it, while Describe keeps the vision model. func TestOpenAICompatibleAdapter_OCRUsesOwnModel(t *testing.T) { t.Parallel() srv, rec := newModelRecorderServer(t) ad := NewOpenAICompatible(config.LLMConfig{ BaseURL: srv.URL, Model: "vision-main", OCRModel: "ocr-special", }) if _, err := ad.Describe(context.Background(), ImageRef{Source: SourceBase64, Data: "aGVsbG8="}, ""); err != nil { t.Fatalf("Describe: %v", err) } if _, err := ad.Extract(context.Background(), ImageRef{Source: SourceBase64, Data: "aGVsbG8="}); err != nil { t.Fatalf("Extract: %v", err) } seen := rec.seen() if len(seen) != 2 || seen[0] != "vision-main" || seen[1] != "ocr-special" { t.Fatalf("models seen = %v, want [vision-main ocr-special]", seen) } } // TestOpenAICompatibleAdapter_OCRWithoutOwnModelFallsBack pins the compatibility // edge: without LLM_OCR_MODEL both directions use the vision model. func TestOpenAICompatibleAdapter_OCRWithoutOwnModelFallsBack(t *testing.T) { t.Parallel() srv, rec := newModelRecorderServer(t) ad := NewOpenAICompatible(config.LLMConfig{BaseURL: srv.URL, Model: "vision-main"}) if _, err := ad.Extract(context.Background(), ImageRef{Source: SourceBase64, Data: "aGVsbG8="}); err != nil { t.Fatalf("Extract: %v", err) } if seen := rec.seen(); len(seen) != 1 || seen[0] != "vision-main" { t.Fatalf("models seen = %v, want [vision-main]", seen) } // WithOCRModel overrides cfg.OCRModel, so an explicit option still wins. ad2 := NewOpenAICompatible(config.LLMConfig{BaseURL: srv.URL, Model: "vision-main", OCRModel: "cfg-ocr"}, WithOCRModel("opt-ocr")) if _, err := ad2.Extract(context.Background(), ImageRef{Source: SourceBase64, Data: "aGVsbG8="}); err != nil { t.Fatalf("Extract: %v", err) } if seen := rec.seen(); len(seen) != 2 || seen[1] != "opt-ocr" { t.Fatalf("models seen = %v, want the explicit option opt-ocr", seen) } }