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