返回 DeepSeek-Reasonix
pi_catalog.go
根目录 / internal / provider / pi_catalog.go
1 package provider
2
3 import (
4 "slices"
5 "sort"
6 "strings"
7
8 piAI "github.com/sky-valley/pi/ai"
9 )
10
11 // PiCatalogModelInfo adapts the embedded sky-valley/pi catalog to Reasonix's
12 // provider-neutral model metadata. Only an exact provider/model and matching
13 // official route are accepted; custom endpoints never inherit catalog facts.
14 func PiCatalogModelInfo(kind, baseURL, model string) (ModelInfo, bool) {
15 route, ok := OfficialOpenCodeGoRoute(kind, baseURL)
16 if !ok {
17 return ModelInfo{}, false
18 }
19 for _, candidate := range piCatalogOpenCodeGoModels(route) {
20 if candidate.ID == strings.TrimSpace(model) {
21 return modelInfoFromPi(candidate), true
22 }
23 }
24 return ModelInfo{}, false
25 }
26
27 // PiCatalogModelInfoForProvider resolves the installed catalog using the
28 // configured provider id when it is an exact pi provider id. Endpoint and API
29 // must also match the catalog entry, preventing a custom gateway from
30 // accidentally inheriting another vendor's metadata.
31 func PiCatalogModelInfoForProvider(providerID, kind, baseURL, model string) (ModelInfo, bool) {
32 providerID = strings.TrimSpace(providerID)
33 if providerID == "" {
34 return ModelInfo{}, false
35 }
36 api := expectedCatalogAPI(kind)
37 configuredURL := strings.TrimRight(strings.TrimSpace(baseURL), "/")
38 if route, ok := OfficialOpenCodeGoRoute(kind, configuredURL); ok && piRouteCorrectedAway(route, strings.TrimSpace(model)) {
39 return ModelInfo{}, false
40 }
41 for _, candidate := range piAI.GetModels(providerID) {
42 if candidate == nil || candidate.ID != strings.TrimSpace(model) || strings.ToLower(strings.TrimSpace(string(candidate.Api))) != api {
43 continue
44 }
45 catalogURL := strings.TrimRight(strings.TrimSpace(candidate.BaseURL), "/")
46 if configuredURL != catalogURL {
47 continue
48 }
49 return modelInfoFromPi(candidate), true
50 }
51 return ModelInfo{}, false
52 }
53
54 func expectedCatalogAPI(kind string) string {
55 switch strings.ToLower(strings.TrimSpace(kind)) {
56 case "openai", "chat", "":
57 return "openai-completions"
58 case "responses":
59 return "openai-responses"
60 case "anthropic":
61 return "anthropic-messages"
62 default:
63 return ""
64 }
65 }
66
67 // PiCatalogModelInfos returns the complete embedded catalog for one pi
68 // provider ID. Callers should still verify that the configured endpoint and
69 // protocol match the catalog provider before applying these facts.
70 func PiCatalogModelInfos(providerID string) []ModelInfo {
71 models := piAI.GetModels(strings.TrimSpace(providerID))
72 out := make([]ModelInfo, 0, len(models))
73 for _, model := range models {
74 if model != nil {
75 out = append(out, modelInfoFromPi(model))
76 }
77 }
78 return out
79 }
80
81 // PiCatalogOpenCodeGoModelIDs returns the exact model IDs exposed by one
82 // OpenCode Go wire route in the embedded catalog.
83 func PiCatalogOpenCodeGoModelIDs(route string) []string {
84 models := piCatalogOpenCodeGoModels(route)
85 ids := make([]string, 0, len(models))
86 for _, model := range models {
87 ids = append(ids, model.ID)
88 }
89 sort.Strings(ids)
90 return ids
91 }
92
93 // PiCatalogOpenCodeGoReasoning exposes catalog wire values, not Pi's clamping
94 // policy. Missing off mappings do not invent a disable token for the adapter.
95 func PiCatalogOpenCodeGoReasoning(route, id string) (ReasoningCapability, bool) {
96 for _, model := range piCatalogOpenCodeGoModels(route) {
97 if model.ID != id {
98 continue
99 }
100 if !model.Reasoning {
101 return ReasoningOptions(""), true
102 }
103 var ids []string
104 for _, level := range piAI.GetSupportedThinkingLevels(model) {
105 value, explicit := model.ThinkingLevelMap[level]
106 wire := string(level)
107 if explicit {
108 if value == nil {
109 continue
110 }
111 wire = *value
112 } else if level == "off" {
113 continue
114 }
115 if wire != "" && !slices.Contains(ids, wire) {
116 ids = append(ids, wire)
117 }
118 }
119 def := "high"
120 if !slices.Contains(ids, def) {
121 def = ""
122 if len(ids) > 0 {
123 def = ids[0]
124 }
125 }
126 return ReasoningOptions(def, ids...), true
127 }
128 return ReasoningCapability{}, false
129 }
130
131 // PiCatalogOpenCodeGoVisionModelIDs returns the route's catalog models that
132 // explicitly accept image input.
133 func PiCatalogOpenCodeGoVisionModelIDs(route string) []string {
134 models := piCatalogOpenCodeGoModels(route)
135 ids := make([]string, 0, len(models))
136 for _, model := range models {
137 if modelInfoFromPi(model).SupportsInput(ModalityImage) {
138 ids = append(ids, model.ID)
139 }
140 }
141 sort.Strings(ids)
142 return ids
143 }
144
145 // piOpenCodeGoRouteCorrections pins models whose Pi route annotation disagrees
146 // with both the opencode.ai/docs/go Endpoints table and models.dev.
147 var piOpenCodeGoRouteCorrections = map[string]string{
148 "minimax-m2.7": OpenCodeGoRouteAnthropic,
149 }
150
151 // OpenCodeGoRouteCorrected reports a model whose route differs from the one
152 // the embedded Pi catalog annotates.
153 func OpenCodeGoRouteCorrected(model string) bool {
154 _, ok := piOpenCodeGoRouteCorrections[strings.TrimSpace(model)]
155 return ok
156 }
157
158 func piRouteCorrectedAway(route, id string) bool {
159 corrected, ok := piOpenCodeGoRouteCorrections[id]
160 return ok && corrected != route
161 }
162
163 func piCatalogOpenCodeGoModels(route string) []*piAI.Model {
164 var wantAPI, wantBaseURL string
165 switch route {
166 case OpenCodeGoRouteChat:
167 wantAPI, wantBaseURL = "openai-completions", "https://opencode.ai/zen/go/v1"
168 case OpenCodeGoRouteAnthropic:
169 wantAPI, wantBaseURL = "anthropic-messages", "https://opencode.ai/zen/go"
170 case OpenCodeGoRouteResponses:
171 wantAPI, wantBaseURL = "openai-responses", "https://opencode.ai/zen/go/v1"
172 default:
173 return nil
174 }
175 models := make([]*piAI.Model, 0)
176 for _, model := range piAI.GetModels("opencode-go") {
177 if model != nil && strings.EqualFold(string(model.Api), wantAPI) && strings.TrimRight(model.BaseURL, "/") == wantBaseURL && !piRouteCorrectedAway(route, model.ID) {
178 models = append(models, model)
179 }
180 }
181 return models
182 }
183
184 func modelInfoFromPi(model *piAI.Model) ModelInfo {
185 modalities := make([]ModelModality, 0, len(model.Input))
186 for _, input := range model.Input {
187 modality := ModelModality(strings.ToLower(strings.TrimSpace(input)))
188 if modality == ModalityText || modality == ModalityImage {
189 modalities = append(modalities, modality)
190 }
191 }
192 if len(modalities) == 0 {
193 modalities = []ModelModality{ModalityText}
194 }
195 return ModelInfo{
196 ID: model.ID,
197 Name: model.Name,
198 API: string(model.Api),
199 BaseURL: model.BaseURL,
200 InputModalities: modalities,
201 ContextWindow: model.ContextWindow,
202 MaxOutputTokens: model.MaxTokens,
203 Reasoning: model.Reasoning,
204 }
205 }
206
206 lines GO