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