1// Package main generates the OpenCode Zen provider configuration.
2package main
3
4import (
5 "cmp"
6 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "log"
11 "math"
12 "net/http"
13 "os"
14 "slices"
15 "strings"
16 "time"
17
18 "charm.land/catwalk/pkg/catwalk"
19)
20
21type ZenModel struct {
22 ID string `json:"id"`
23 Object string `json:"object"`
24 Created int64 `json:"created"`
25 OwnedBy string `json:"owned_by"`
26}
27
28type ZenModelsResponse struct {
29 Object string `json:"object"`
30 Data []ZenModel `json:"data"`
31}
32
33type PricingData struct {
34 Input float64 `json:"input"`
35 Output float64 `json:"output"`
36 CacheRead float64 `json:"cache_read,omitempty"`
37 CacheWrite float64 `json:"cache_write,omitempty"`
38}
39
40type ModelLimit struct {
41 Context int64 `json:"context"`
42 Output int64 `json:"output"`
43}
44
45type ModelEnrichment struct {
46 Name string `json:"name"`
47 Attachment bool `json:"attachment"`
48 Reasoning bool `json:"reasoning"`
49 Cost PricingData `json:"cost"`
50 Limit ModelLimit `json:"limit"`
51}
52
53func fetchZenModels() ([]ZenModel, error) {
54 apiKey := cmp.Or(os.Getenv("OPENCODE_API_KEY"), "public")
55
56 client := &http.Client{Timeout: 30 * time.Second}
57 req, _ := http.NewRequestWithContext(
58 context.Background(),
59 "GET",
60 "https://opencode.ai/zen/v1/models",
61 nil,
62 )
63 req.Header.Set("User-Agent", "Catwalk/1.0")
64 req.Header.Set("Authorization", "Bearer "+apiKey)
65
66 resp, err := client.Do(req)
67 if err != nil {
68 return nil, fmt.Errorf("failed to fetch zen models: %w", err)
69 }
70 defer func() { _ = resp.Body.Close() }()
71 if resp.StatusCode != 200 {
72 body, _ := io.ReadAll(resp.Body)
73 return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
74 }
75
76 var mr ZenModelsResponse
77 if err := json.NewDecoder(resp.Body).Decode(&mr); err != nil {
78 return nil, fmt.Errorf("failed to decode zen models: %w", err)
79 }
80
81 return mr.Data, nil
82}
83
84func fetchEnrichmentData() (map[string]ModelEnrichment, error) {
85 client := &http.Client{Timeout: 30 * time.Second}
86 req, _ := http.NewRequestWithContext(
87 context.Background(),
88 "GET",
89 "https://models.dev/api.json",
90 nil,
91 )
92 req.Header.Set("User-Agent", "Catwalk/1.0")
93
94 resp, err := client.Do(req)
95 if err != nil {
96 return nil, fmt.Errorf("failed fetching enrichment data: %w", err)
97 }
98 defer func() { _ = resp.Body.Close() }()
99 if resp.StatusCode != 200 {
100 body, _ := io.ReadAll(resp.Body)
101 return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
102 }
103
104 var fullData map[string]json.RawMessage
105 if err := json.NewDecoder(resp.Body).Decode(&fullData); err != nil {
106 return nil, fmt.Errorf("failed to decode when fetching enrichment data: %w", err)
107 }
108
109 rawOpenCode, ok := fullData["opencode"]
110 if !ok {
111 return nil, fmt.Errorf("opencode provider not found in models.dev/api.json")
112 }
113
114 var openCodeData struct {
115 Models map[string]ModelEnrichment `json:"models"`
116 }
117 if err := json.Unmarshal(rawOpenCode, &openCodeData); err != nil {
118 return nil, fmt.Errorf("failed to unmarshal when fetching enrichment data: %w", err)
119 }
120
121 return openCodeData.Models, nil
122}
123
124func main() {
125 zenModels, err := fetchZenModels()
126 if err != nil {
127 log.Fatal("Error fetching OpenCode Zen models:", err)
128 }
129
130 enrichmentData, err := fetchEnrichmentData()
131 if err != nil {
132 log.Fatal("Error fetching enrichment data:", err)
133 }
134
135 zenProvider := catwalk.Provider{
136 Name: "OpenCode Zen",
137 ID: catwalk.InferenceProviderOpenCodeZen,
138 APIKey: "$OPENCODE_API_KEY",
139 APIEndpoint: "https://opencode.ai/zen/v1",
140 Type: catwalk.TypeOpenAICompat,
141 DefaultLargeModelID: "minimax-m2.5-free",
142 DefaultSmallModelID: "minimax-m2.5-free",
143 }
144
145 for _, zenModel := range zenModels {
146 enrichment, hasEnrichment := enrichmentData[zenModel.ID]
147
148 var costPer1MIn, costPer1MOut, costPer1MInCached, costPer1MOutCached float64
149 var contextWindow, defaultMaxTokens int64 = 200000, 20000
150 var supportsImages bool
151 var canReason bool
152 var reasoningLevels []string
153 var defaultReasoningEffort string
154 modelName := zenModel.ID
155
156 if hasEnrichment {
157 costPer1MIn = math.Round(enrichment.Cost.Input*100) / 100
158 costPer1MOut = math.Round(enrichment.Cost.Output*100) / 100
159 costPer1MInCached = math.Round(enrichment.Cost.CacheRead*100) / 100
160 costPer1MOutCached = math.Round(enrichment.Cost.CacheWrite*100) / 100
161 contextWindow = enrichment.Limit.Context
162 defaultMaxTokens = enrichment.Limit.Output
163 supportsImages = enrichment.Attachment
164 modelName = enrichment.Name
165
166 if enrichment.Reasoning {
167 reasoningLevels = []string{"low", "medium", "high"}
168 defaultReasoningEffort = "medium"
169 canReason = true
170 }
171 } else {
172 log.Printf("WARNING: No enrichment found for model %s, using defaults\n", zenModel.ID)
173 }
174
175 m := catwalk.Model{
176 ID: zenModel.ID,
177 Name: modelName,
178 CostPer1MIn: costPer1MIn,
179 CostPer1MOut: costPer1MOut,
180 CostPer1MInCached: costPer1MInCached,
181 CostPer1MOutCached: costPer1MOutCached,
182 ContextWindow: contextWindow,
183 DefaultMaxTokens: defaultMaxTokens,
184 SupportsImages: supportsImages,
185 CanReason: canReason,
186 ReasoningLevels: reasoningLevels,
187 DefaultReasoningEffort: defaultReasoningEffort,
188 }
189
190 zenProvider.Models = append(zenProvider.Models, m)
191 fmt.Printf("Added model %s (%s)\n", zenModel.ID, modelName)
192 }
193
194 slices.SortFunc(zenProvider.Models, func(a catwalk.Model, b catwalk.Model) int {
195 return strings.Compare(a.Name, b.Name)
196 })
197
198 data, err := json.MarshalIndent(zenProvider, "", " ")
199 if err != nil {
200 log.Fatal("Error marshaling provider:", err)
201 }
202 data = append(data, '\n')
203
204 if err := os.WriteFile("internal/providers/configs/opencode-zen.json", data, 0o600); err != nil {
205 log.Fatal("Error writing provider config:", err)
206 }
207
208 fmt.Printf("Generated opencode-zen.json with %d models\n", len(zenProvider.Models))
209}