返回 DeepSeek-Reasonix
vision.go
根目录 / internal / config / vision.go
1 package config
2
3 import (
4 "net/url"
5 "strings"
6
7 "reasonix/internal/provider/openai"
8 )
9
10 var mimoVisionModels = map[string]bool{
11 "mimo-v2.5": true,
12 "mimo-v2-omni": true,
13 }
14
15 // InferVisionModels returns model IDs that look like chat models with image-input
16 // support. It is intentionally conservative and meant for Settings defaults; an
17 // explicit provider vision_models list remains the source of truth.
18 func InferVisionModels(models []string) []string {
19 out := make([]string, 0, len(models))
20 seen := map[string]bool{}
21 for _, model := range models {
22 model = strings.TrimSpace(model)
23 if model == "" || seen[model] || !IsLikelyChatModel(model) || !IsLikelyVisionModel(model) {
24 continue
25 }
26 seen[model] = true
27 out = append(out, model)
28 }
29 return out
30 }
31
32 func IsLikelyVisionModel(model string) bool {
33 model = strings.TrimSpace(model)
34 if model == "" {
35 return false
36 }
37 lower := strings.ToLower(model)
38 if mimoVisionModels[lower] {
39 return true
40 }
41 tokens := strings.FieldsFunc(lower, modelTokenSeparator)
42 for _, token := range tokens {
43 if token == "audio" {
44 return false
45 }
46 }
47 if strings.HasPrefix(lower, "gpt-4o") {
48 return true
49 }
50 for _, token := range tokens {
51 switch token {
52 case "vl", "vision", "visual", "multimodal", "omni":
53 return true
54 }
55 }
56 return false
57 }
58
59 func modelTokenSeparator(r rune) bool {
60 return r == '-' || r == '_' || r == '.' || r == '/' || r == ':'
61 }
62
63 // EffectiveVision resolves whether the selected model accepts image input.
64 // Explicit provider vision still wins for custom vision-capable gateways; the
65 // MiMo endpoint heuristic is deliberately limited to known MiMo endpoints so
66 // arbitrary OpenAI-compatible proxies do not get image payloads unexpectedly.
67 func EffectiveVision(e *ProviderEntry) bool {
68 if e == nil {
69 return false
70 }
71 if enabled, explicit := explicitModelVision(e); explicit {
72 return enabled
73 }
74 // DeepSeek's official APIs currently accept text message content only. Treat
75 // current official models as text-only when only the legacy provider-wide
76 // vision flag is set, so stale configs cannot make requests 400. A concrete
77 // model can still opt in through vision_models or a model override when
78 // DeepSeek documents a future multimodal request shape.
79 if openai.IsDeepSeek(e.BaseURL) {
80 return false
81 }
82 if e.Vision {
83 return true
84 }
85 return isOfficialMimoVisionEntry(e)
86 }
87
88 // ExplicitModelVision reports whether the selected model has a positive,
89 // model-scoped image capability declaration. Provider assembly uses this
90 // provenance to distinguish a deliberate future-model opt-in from a stale
91 // provider-wide vision=true setting.
92 func ExplicitModelVision(e *ProviderEntry) bool {
93 enabled, explicit := explicitModelVision(e)
94 return explicit && enabled
95 }
96
97 func explicitModelVision(e *ProviderEntry) (enabled, explicit bool) {
98 if e == nil {
99 return false, false
100 }
101 if e.visionOverride != nil {
102 return *e.visionOverride, true
103 }
104 if e.HasVisionModel(e.Model) {
105 return true, true
106 }
107 return false, false
108 }
109
110 func (e *ProviderEntry) HasVisionModel(model string) bool {
111 model = strings.TrimSpace(model)
112 if model == "" {
113 return false
114 }
115 for _, candidate := range e.VisionModels {
116 if strings.EqualFold(strings.TrimSpace(candidate), model) {
117 return true
118 }
119 }
120 return false
121 }
122
123 func isOfficialMimoVisionEntry(e *ProviderEntry) bool {
124 if !isOpenAIProviderKind(e) || !mimoVisionModels[strings.ToLower(strings.TrimSpace(e.Model))] {
125 return false
126 }
127 switch officialMimoHost(e.BaseURL) {
128 case "api.xiaomimimo.com", "token-plan-cn.xiaomimimo.com":
129 return true
130 default:
131 return false
132 }
133 }
134
135 func officialMimoHost(baseURL string) string {
136 u, err := url.Parse(strings.TrimSpace(baseURL))
137 if err != nil {
138 return ""
139 }
140 return strings.ToLower(u.Hostname())
141 }
142
142 lines GO