返回 DeepSeek-Reasonix
opencode_go.go
根目录 / internal / provider / opencode_go.go
1 package provider
2
3 import (
4 "maps"
5 "net/url"
6 "sort"
7 "strings"
8 )
9
10 // Official OpenCode Go routes verified against the public endpoint list.
11 // Similar hostnames, custom proxies, and extra query/userinfo never match.
12 const (
13 openCodeGoHost = "opencode.ai"
14 openCodeGoChatPath = "/zen/go/v1"
15 openCodeGoAnthropicPath = "/zen/go"
16 )
17
18 // OpenCodeGoModelLimits is the static models.dev snapshot used by both config
19 // presets and provider policy. There is no runtime network fetch.
20 type OpenCodeGoModelLimits struct {
21 Context int
22 MaxOutput int
23 }
24
25 const (
26 OpenCodeGoRouteChat = "chat"
27 OpenCodeGoRouteAnthropic = "anthropic"
28 OpenCodeGoRouteResponses = "responses"
29 )
30
31 // OpenCodeGoChatModels is the official Chat Completions catalog.
32 func OpenCodeGoChatModels() map[string]OpenCodeGoModelLimits {
33 return mergeOpenCodeGoLimits(OpenCodeGoRouteChat, map[string]OpenCodeGoModelLimits{
34 "glm-5.3": {Context: 1_000_000, MaxOutput: 131_072}, "glm-5.2": {Context: 1_000_000, MaxOutput: 131_072},
35 "glm-5.1": {Context: 202_752, MaxOutput: 32_768}, "kimi-k3": {Context: 1_048_576, MaxOutput: 131_072},
36 "kimi-k2.7-code": {Context: 262_144, MaxOutput: 262_144}, "kimi-k2.6": {Context: 262_144, MaxOutput: 65_536},
37 "deepseek-flash": {Context: 1_000_000, MaxOutput: 384_000}, "deepseek-v4-pro": {Context: 1_000_000, MaxOutput: 384_000}, "deepseek-v4-flash": {Context: 1_000_000, MaxOutput: 384_000},
38 "deepseek-v4-flash-vision-exp": {Context: 1_000_000, MaxOutput: 384_000}, "mimo-v2.5-pro": {Context: 1_048_576, MaxOutput: 128_000},
39 "mimo-v2.5": {Context: 1_000_000, MaxOutput: 128_000}, "hy3": {Context: 256_000, MaxOutput: 64_000},
40 })
41 }
42
43 // OpenCodeGoAnthropicModels is the official Anthropic-compatible catalog.
44 func OpenCodeGoAnthropicModels() map[string]OpenCodeGoModelLimits {
45 return mergeOpenCodeGoLimits(OpenCodeGoRouteAnthropic, map[string]OpenCodeGoModelLimits{
46 "qwen3.8-max": {Context: 1_000_000, MaxOutput: 131_072}, "qwen3.7-max": {Context: 1_000_000, MaxOutput: 65_536},
47 "qwen3.7-plus": {Context: 1_000_000, MaxOutput: 65_536}, "qwen3.6-plus": {Context: 1_000_000, MaxOutput: 65_536},
48 "minimax-m3": {Context: 1_000_000, MaxOutput: 131_072}, "minimax-m2.7": {Context: 204_800, MaxOutput: 131_072},
49 "minimax-m2.5": {Context: 204_800, MaxOutput: 65_536},
50 })
51 }
52
53 // OpenCodeGoResponsesModels is the official Responses API catalog. DeepSeek's
54 // separately verified alternative Responses route remains supported below.
55 func OpenCodeGoResponsesModels() map[string]OpenCodeGoModelLimits {
56 return mergeOpenCodeGoLimits(OpenCodeGoRouteResponses, map[string]OpenCodeGoModelLimits{
57 "grok-4.5": {Context: 500_000, MaxOutput: 500_000}, "gpt-5.6-luna": {Context: 1_050_000, MaxOutput: 128_000},
58 "muse-spark-1.2-contributor": {Context: 1_048_576, MaxOutput: 131_072},
59 })
60 }
61
62 func mergeOpenCodeGoLimits(route string, compatibility map[string]OpenCodeGoModelLimits) map[string]OpenCodeGoModelLimits {
63 out := make(map[string]OpenCodeGoModelLimits, len(compatibility))
64 maps.Copy(out, compatibility)
65 for _, model := range piCatalogOpenCodeGoModels(route) {
66 out[model.ID] = OpenCodeGoModelLimits{Context: model.ContextWindow, MaxOutput: model.MaxTokens}
67 }
68 return out
69 }
70
71 // OpenCodeGoModelIDs returns the union of the embedded Pi catalog and the
72 // verified compatibility catalog, preserving models still served by the
73 // endpoint even if one catalog snapshot lags the other.
74 func OpenCodeGoModelIDs(route string) []string {
75 var models map[string]OpenCodeGoModelLimits
76 switch route {
77 case OpenCodeGoRouteChat:
78 models = OpenCodeGoChatModels()
79 case OpenCodeGoRouteAnthropic:
80 models = OpenCodeGoAnthropicModels()
81 case OpenCodeGoRouteResponses:
82 models = OpenCodeGoResponsesModels()
83 }
84 ids := make([]string, 0, len(models))
85 for id := range models {
86 ids = append(ids, id)
87 }
88 sort.Strings(ids)
89 return ids
90 }
91
92 func OpenCodeGoVisionModelIDs(route string) []string {
93 ids := OpenCodeGoModelIDs(route)
94 out := make([]string, 0, len(ids))
95 for _, id := range ids {
96 if info, ok := OpenCodeGoModelInfo(routeKind(route), routeURL(route), id); ok && info.SupportsInput(ModalityImage) {
97 out = append(out, id)
98 }
99 }
100 return out
101 }
102
103 func routeKind(route string) string {
104 switch route {
105 case OpenCodeGoRouteAnthropic:
106 return "anthropic"
107 case OpenCodeGoRouteResponses:
108 return "responses"
109 default:
110 return "openai"
111 }
112 }
113
114 func routeURL(route string) string {
115 if route == OpenCodeGoRouteAnthropic {
116 return "https://opencode.ai/zen/go"
117 }
118 return "https://opencode.ai/zen/go/v1"
119 }
120
121 func officialOpenCodeGoPath(baseURL string) (string, bool) {
122 u, err := url.Parse(strings.TrimSpace(baseURL))
123 if err != nil || !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Hostname(), openCodeGoHost) ||
124 u.Port() != "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
125 return "", false
126 }
127 return strings.TrimRight(u.EscapedPath(), "/"), true
128 }
129
130 // OfficialOpenCodeGoRoute reports the exact official route for kind+URL.
131 // kind is openai/chat, anthropic, or responses. Unknown kind/URL is false.
132 func OfficialOpenCodeGoRoute(kind, baseURL string) (string, bool) {
133 path, ok := officialOpenCodeGoPath(baseURL)
134 if !ok {
135 return "", false
136 }
137 switch strings.ToLower(strings.TrimSpace(kind)) {
138 case "openai", "chat", "":
139 if path == openCodeGoChatPath {
140 return OpenCodeGoRouteChat, true
141 }
142 case "responses":
143 if path == openCodeGoChatPath {
144 return OpenCodeGoRouteResponses, true
145 }
146 case "anthropic":
147 if path == openCodeGoAnthropicPath {
148 return OpenCodeGoRouteAnthropic, true
149 }
150 }
151 return "", false
152 }
153
154 func lookupOpenCodeGoLimits(table map[string]OpenCodeGoModelLimits, model string) (OpenCodeGoModelLimits, bool) {
155 model = strings.ToLower(strings.TrimSpace(model))
156 if model == "" {
157 return OpenCodeGoModelLimits{}, false
158 }
159 for id, lim := range table {
160 if strings.ToLower(id) == model {
161 return lim, true
162 }
163 }
164 return OpenCodeGoModelLimits{}, false
165 }
166
167 func isDeepSeekModelID(model string) bool {
168 return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "deepseek-")
169 }
170
171 // LookupOfficialOpenCodeGo returns static limits only for the official
172 // endpoint and a model listed for that route. Future/unknown models miss.
173 func LookupOfficialOpenCodeGo(kind, baseURL, model string) (OpenCodeGoModelLimits, bool) {
174 route, ok := OfficialOpenCodeGoRoute(kind, baseURL)
175 if !ok {
176 return OpenCodeGoModelLimits{}, false
177 }
178 switch route {
179 case OpenCodeGoRouteChat:
180 return lookupOpenCodeGoLimits(OpenCodeGoChatModels(), model)
181 case OpenCodeGoRouteAnthropic:
182 if lim, ok := lookupOpenCodeGoLimits(OpenCodeGoAnthropicModels(), model); ok {
183 return lim, true
184 }
185 if isDeepSeekModelID(model) {
186 return lookupOpenCodeGoLimits(OpenCodeGoChatModels(), model)
187 }
188 case OpenCodeGoRouteResponses:
189 if lim, ok := lookupOpenCodeGoLimits(OpenCodeGoResponsesModels(), model); ok {
190 return lim, true
191 }
192 if isDeepSeekModelID(model) {
193 return lookupOpenCodeGoLimits(OpenCodeGoChatModels(), model)
194 }
195 }
196 return OpenCodeGoModelLimits{}, false
197 }
198
199 // OpenCodeGoModelInfo returns the adapter-owned model metadata for the exact
200 // official OpenCode Go route. It intentionally returns text-only for every
201 // listed model unless the local adapter has an explicit image declaration.
202 func OpenCodeGoModelInfo(kind, baseURL, model string) (ModelInfo, bool) {
203 if info, ok := PiCatalogModelInfo(kind, baseURL, model); ok {
204 return info, true
205 }
206 route, ok := OfficialOpenCodeGoRoute(kind, baseURL)
207 if !ok {
208 return ModelInfo{}, false
209 }
210 if _, ok := LookupOfficialOpenCodeGo(kind, baseURL, model); !ok {
211 return ModelInfo{}, false
212 }
213 info := ModelInfo{ID: strings.TrimSpace(model), InputModalities: []ModelModality{ModalityText}}
214 if model == "deepseek-v4-flash-vision-exp" {
215 info.InputModalities = []ModelModality{ModalityText, ModalityImage}
216 return info, true
217 }
218 vision := map[string]map[string]bool{
219 OpenCodeGoRouteChat: {"kimi-k3": true},
220 OpenCodeGoRouteAnthropic: {"qwen3.8-max": true, "qwen3.7-plus": true, "qwen3.6-plus": true},
221 OpenCodeGoRouteResponses: {"grok-4.5": true, "gpt-5.6-luna": true, "muse-spark-1.2-contributor": true},
222 }[route]
223 if vision[strings.ToLower(strings.TrimSpace(model))] {
224 info.InputModalities = []ModelModality{ModalityText, ModalityImage}
225 }
226 return info, true
227 }
228
229 // BuiltinModelInfo is the shared local catalog seam for built-in adapters.
230 // It is deliberately exact-match and returns false for unknown/custom routes.
231 func BuiltinModelInfo(kind, baseURL, model string) (ModelInfo, bool) {
232 if info, ok := OpenCodeGoModelInfo(kind, baseURL, model); ok {
233 return info, true
234 }
235 if info, ok := ModelScopeModelInfo(kind, baseURL, model); ok {
236 return info, true
237 }
238 if strings.EqualFold(strings.TrimSpace(kind), "openai") || strings.EqualFold(strings.TrimSpace(kind), "responses") || strings.EqualFold(strings.TrimSpace(kind), "anthropic") {
239 u, err := url.Parse(strings.TrimSpace(baseURL))
240 if err == nil && strings.EqualFold(u.Scheme, "https") && strings.EqualFold(u.Hostname(), "api.deepseek.com") {
241 id := strings.TrimSpace(model)
242 if IsOfficialDeepSeekImageModel(id) {
243 return ModelInfo{ID: id, InputModalities: []ModelModality{ModalityText, ModalityImage}}, true
244 }
245 if IsOfficialDeepSeekTextModel(id) {
246 return ModelInfo{ID: id, InputModalities: []ModelModality{ModalityText}}, true
247 }
248 }
249 }
250 return ModelInfo{}, false
251 }
252
253 // ModelScopeModelInfo is the local catalog for the curated ModelScope route.
254 // The endpoint currently exposes an OpenAI-compatible API without reliable
255 // capability fields, so only the verified Qwen3.5 SKUs are image-capable.
256 func ModelScopeModelInfo(kind, baseURL, model string) (ModelInfo, bool) {
257 if !strings.EqualFold(strings.TrimSpace(kind), "openai") {
258 return ModelInfo{}, false
259 }
260 u, err := url.Parse(strings.TrimSpace(baseURL))
261 if err != nil || !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Hostname(), "api-inference.modelscope.cn") || u.Port() != "" {
262 return ModelInfo{}, false
263 }
264 known := map[string]bool{
265 "qwen/qwen3.5-397b-a17b": true,
266 "qwen/qwen3.5-122b-a10b": true,
267 "qwen/qwen3.5-27b": true,
268 "deepseek-ai/deepseek-v4-flash-0731": false,
269 "deepseek-ai/deepseek-v4-pro": false,
270 "minimax/minimax-m3": false,
271 "zhipuai/glm-5.2": false,
272 }
273 id := strings.TrimSpace(model)
274 vision, ok := known[strings.ToLower(id)]
275 if !ok {
276 return ModelInfo{}, false
277 }
278 modalities := []ModelModality{ModalityText}
279 if vision {
280 modalities = append(modalities, ModalityImage)
281 }
282 return ModelInfo{ID: id, InputModalities: modalities}, true
283 }
284
285 // FilterOfficialOpenCodeGoModels removes models that the shared OpenCode Go
286 // /v1/models catalog exposes for a different wire format. Custom endpoints are
287 // returned unchanged because Reasonix cannot infer their routing policy.
288 func FilterOfficialOpenCodeGoModels(kind, baseURL string, models []string) []string {
289 if _, ok := OfficialOpenCodeGoRoute(kind, baseURL); !ok {
290 return models
291 }
292 out := make([]string, 0, len(models))
293 for _, model := range models {
294 if _, ok := LookupOfficialOpenCodeGo(kind, baseURL, model); ok {
295 out = append(out, model)
296 }
297 }
298 return out
299 }
300
300 lines GO