返回 DeepSeek-Reasonix
host.go
根目录 / internal / provider / openai / host.go
1 package openai
2
3 import (
4 "net/url"
5 "slices"
6 "strings"
7
8 "reasonix/internal/provider"
9 )
10
11 // matchesVendorHost reports whether baseURL points at one of the canonical
12 // hostnames (exact match, case-insensitive) or at any subdomain of apex.
13 // Returns false on any parse error or empty host.
14 //
15 // We take the apex separately from the canonical because they differ: the
16 // canonical (e.g. api.minimaxi.com) is the specific endpoint, but regional
17 // subdomains like eu.minimaxi.com or us.minimaxi.com should also match —
18 // the wire shape is the same, just hosted in a different region. The bare
19 // apex (e.g. minimaxi.com) is intentionally rejected: it would only happen
20 // if the user pointed their base_url at the apex domain, which is a
21 // misconfiguration — not a path we want to silently accept.
22 func matchesVendorHost(baseURL, apex string, canonical ...string) bool {
23 u, err := url.Parse(baseURL)
24 if err != nil {
25 return false
26 }
27 host := strings.ToLower(u.Hostname())
28 if slices.Contains(canonical, host) {
29 return true
30 }
31 return strings.HasSuffix(host, "."+apex)
32 }
33
34 // IsDeepSeek reports whether baseURL points at DeepSeek's API
35 // (api.deepseek.com or any *.deepseek.com subdomain).
36 func IsDeepSeek(baseURL string) bool {
37 return matchesVendorHost(baseURL, "deepseek.com", "api.deepseek.com")
38 }
39
40 // OfficialDeepSeekVisionModel has built-in image support. Unknown models need
41 // capability metadata or an explicit declaration, not a name-based guess.
42 const OfficialDeepSeekVisionModel = provider.OfficialDeepSeekVisionModel
43
44 // IsOfficialDeepSeekVisionModel reports whether model is the pinned official
45 // DeepSeek vision SKU. Matching is case-insensitive and trims surrounding space.
46 func IsOfficialDeepSeekVisionModel(model string) bool {
47 return strings.EqualFold(strings.TrimSpace(model), OfficialDeepSeekVisionModel)
48 }
49
50 // The official DeepSeek model lists live in the provider package so the local
51 // model catalog can consult them without an openai import cycle.
52 func IsOfficialDeepSeekImageModel(model string) bool {
53 return provider.IsOfficialDeepSeekImageModel(model)
54 }
55
56 func IsOfficialDeepSeekTextModel(model string) bool {
57 return provider.IsOfficialDeepSeekTextModel(model)
58 }
59
60 // DeepSeekImageInputAllowed applies the official endpoint hard limit after a
61 // provider has resolved its configured or catalog-derived image capability.
62 func DeepSeekImageInputAllowed(officialBase bool, requestURL, model string, metadataProvided, enabled bool) bool {
63 if !officialBase && !IsDeepSeek(requestURL) {
64 return enabled
65 }
66 if IsOfficialDeepSeekTextModel(model) {
67 return false
68 }
69 return enabled || (!metadataProvided && IsOfficialDeepSeekImageModel(model))
70 }
71
72 // OfficialDeepSeekAllowsVision reports whether this official DeepSeek endpoint
73 // may serialize image parts for the selected model. Custom gateways never match.
74 func OfficialDeepSeekAllowsVision(baseURL, model string) bool {
75 return IsDeepSeek(baseURL) && IsOfficialDeepSeekImageModel(model)
76 }
77
78 // IsOpenAI reports whether baseURL points at OpenAI's official API host. Keep
79 // this exact-host so a compatible gateway under another openai.com subdomain
80 // cannot accidentally receive the official max_completion_tokens wire shape.
81 func IsOpenAI(baseURL string) bool {
82 u, err := url.Parse(baseURL)
83 if err != nil {
84 return false
85 }
86 return strings.EqualFold(u.Hostname(), "api.openai.com")
87 }
88
89 // deepSeekPrefixChatURL returns the official Beta chat endpoint that enables
90 // assistant-prefix completion. Derive it only from a URL already hosted by
91 // DeepSeek: custom gateways may opt into the DeepSeek reasoning wire shape, but
92 // must never be bypassed by an automatic request to the vendor's direct API.
93 func deepSeekPrefixChatURL(chatURL string) string {
94 if !IsDeepSeek(chatURL) {
95 return ""
96 }
97 u, err := url.Parse(strings.TrimSpace(chatURL))
98 if err != nil || u.Scheme == "" || u.Host == "" {
99 return ""
100 }
101 u.Path = "/beta/chat/completions"
102 u.RawPath = ""
103 u.RawQuery = ""
104 u.Fragment = ""
105 return u.String()
106 }
107
108 // IsGeminiAPI reports whether baseURL points at Google's Gemini Developer API.
109 // Keep this exact-host: other googleapis.com services do not share Gemini's
110 // model resource-name compatibility quirk.
111 func IsGeminiAPI(baseURL string) bool {
112 u, err := url.Parse(baseURL)
113 if err != nil {
114 return false
115 }
116 return strings.EqualFold(u.Hostname(), "generativelanguage.googleapis.com")
117 }
118
119 // usesGeminiThoughtSignatures reports whether the current endpoint/model speaks
120 // Gemini's OpenAI-compatible thought-signature extension. The official endpoint
121 // is authoritative even when a custom model alias is used; compatible gateways
122 // are detected from the model ID they route (for example google/gemini-3-pro).
123 // Keeping this decision on the current client prevents a Gemini-authored history
124 // from leaking extra_content.google fields after a same-session provider switch.
125 func usesGeminiThoughtSignatures(baseURL, model string) bool {
126 if IsGeminiAPI(baseURL) {
127 return true
128 }
129 for _, segment := range strings.FieldsFunc(strings.ToLower(strings.TrimSpace(model)), func(r rune) bool {
130 return r == '/' || r == ':'
131 }) {
132 if segment == "gemini" || strings.HasPrefix(segment, "gemini-") || strings.HasPrefix(segment, "gemini_") {
133 return true
134 }
135 }
136 return false
137 }
138
139 // normalizeModelID converts Gemini's resource-form model names returned by some
140 // /models responses into the bare IDs required by OpenAI-compatible chat calls.
141 // Other providers and already-normalized Gemini IDs pass through unchanged.
142 func normalizeModelID(baseURL, model string) string {
143 model = strings.TrimSpace(model)
144 if IsGeminiAPI(baseURL) {
145 model = strings.TrimPrefix(model, "models/")
146 }
147 return model
148 }
149
150 // Explicit official beta alias verified against Chat Completions. Keep
151 // configuration identity exact; never case-fold arbitrary IDs or gateway calls.
152 func deepSeekChatWireModel(endpoint, model string) string {
153 u, err := url.Parse(endpoint)
154 if err == nil && u.Scheme == "https" && u.Host == "api.deepseek.com" &&
155 u.User == nil && u.RawQuery == "" && u.Fragment == "" &&
156 (u.Path == "/chat/completions" || u.Path == "/v1/chat/completions") &&
157 model == "DeepSeek-V4.1-Flash-Expires-On-0910" {
158 return "deepseek-v4.1-flash-expires-on-0910"
159 }
160 return model
161 }
162
163 // IsMiniMax reports whether baseURL points at MiniMax's OpenAI-compatible
164 // endpoint (api.minimaxi.com or any *.minimaxi.com subdomain).
165 //
166 // The host string is matched exactly — the spelling is `minimaxi`, not
167 // `minimax` — to avoid clashing with any future minimax-branded gateway.
168 func IsMiniMax(baseURL string) bool {
169 return matchesVendorHost(baseURL, "minimaxi.com", "api.minimaxi.com")
170 }
171
172 // IsMiMo reports whether baseURL points at Xiaomi MiMo's OpenAI-compatible API.
173 // MiMo follows the OpenAI chat shape but authenticates with an `api-key` header
174 // instead of the usual Authorization bearer header.
175 func IsMiMo(baseURL string) bool {
176 return provider.IsMiMoEndpoint(baseURL)
177 }
178
179 // IsZhipu reports whether baseURL points at Zhipu's OpenAI-compatible endpoint
180 // for GLM models — either the China host (open.bigmodel.cn, *.bigmodel.cn) or
181 // the international Z.ai host (api.z.ai, *.z.ai). Both speak the same wire shape,
182 // where chain-of-thought is gated by `thinking.type` (enabled|disabled) and
183 // `reasoning_effort` is silently ignored, so the client routes reasoning control
184 // to the thinking knob for either host.
185 func IsZhipu(baseURL string) bool {
186 return matchesVendorHost(baseURL, "bigmodel.cn", "open.bigmodel.cn") ||
187 matchesVendorHost(baseURL, "z.ai", "api.z.ai")
188 }
189
190 // IsTokenRhythm reports whether baseURL points at Token Rhythm's official
191 // OpenAI-compatible gateway. Keep this exact-host: model-aware protocol
192 // upgrades must not affect unrelated subdomains or similarly named relays.
193 func IsTokenRhythm(baseURL string) bool {
194 u, err := url.Parse(baseURL)
195 if err != nil {
196 return false
197 }
198 return strings.EqualFold(u.Hostname(), "tokenrhythm.studio")
199 }
200
201 // IsLongCat reports whether baseURL points at LongCat's OpenAI-compatible API.
202 // LongCat uses the OpenAI chat shape, but gates thinking with thinking.type
203 // enabled|disabled rather than the generic reasoning_effort field.
204 func IsLongCat(baseURL string) bool {
205 return matchesVendorHost(baseURL, "longcat.chat", "api.longcat.chat")
206 }
207
208 // IsKimiAPI reports whether baseURL is one of Moonshot's official Kimi direct
209 // API endpoints. Gate Kimi-specific wire compatibility on the exact API hosts
210 // so OpenAI-compatible relays carrying the same model ID remain untouched.
211 func IsKimiAPI(baseURL string) bool {
212 u, err := url.Parse(baseURL)
213 if err != nil {
214 return false
215 }
216 switch strings.ToLower(u.Hostname()) {
217 case "api.moonshot.cn", "api.moonshot.ai":
218 return true
219 default:
220 return false
221 }
222 }
223
224 // IsOllamaCloud reports whether baseURL points at Ollama Cloud's hosted
225 // OpenAI-compatible endpoint. Local Ollama servers intentionally do not match:
226 // the hosted API accepts the reasoning_effort=max extension, while localhost
227 // deployments vary by model/version.
228 func IsOllamaCloud(baseURL string) bool {
229 return matchesVendorHost(baseURL, "ollama.com", "ollama.com")
230 }
231
231 lines GO