1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "log"
8 "net/http"
9 "os"
10 "slices"
11 "strconv"
12 "time"
13
14 "github.com/charmbracelet/fur/internal/providers"
15)
16
17// Model represents the complete model configuration
18type Model struct {
19 ID string `json:"id"`
20 CanonicalSlug string `json:"canonical_slug"`
21 HuggingFaceID string `json:"hugging_face_id"`
22 Name string `json:"name"`
23 Created int64 `json:"created"`
24 Description string `json:"description"`
25 ContextLength int64 `json:"context_length"`
26 Architecture Architecture `json:"architecture"`
27 Pricing Pricing `json:"pricing"`
28 TopProvider TopProvider `json:"top_provider"`
29 SupportedParams []string `json:"supported_parameters"`
30}
31
32// Architecture defines the model's architecture details
33type Architecture struct {
34 Modality string `json:"modality"`
35 InputModalities []string `json:"input_modalities"`
36 OutputModalities []string `json:"output_modalities"`
37 Tokenizer string `json:"tokenizer"`
38 InstructType *string `json:"instruct_type"`
39}
40
41// Pricing contains the pricing information for different operations
42type Pricing struct {
43 Prompt string `json:"prompt"`
44 Completion string `json:"completion"`
45 Request string `json:"request"`
46 Image string `json:"image"`
47 WebSearch string `json:"web_search"`
48 InternalReasoning string `json:"internal_reasoning"`
49 InputCacheRead string `json:"input_cache_read"`
50 InputCacheWrite string `json:"input_cache_write"`
51}
52
53// TopProvider describes the top provider's capabilities
54type TopProvider struct {
55 ContextLength int64 `json:"context_length"`
56 MaxCompletionTokens *int64 `json:"max_completion_tokens"`
57 IsModerated bool `json:"is_moderated"`
58}
59
60type ModelsResponse struct {
61 Data []Model `json:"data"`
62}
63type ModelPricing struct {
64 CostPer1MIn float64 `json:"cost_per_1m_in"`
65 CostPer1MOut float64 `json:"cost_per_1m_out"`
66 CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
67 CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
68}
69
70func getPricing(model Model) ModelPricing {
71 pricing := ModelPricing{}
72 costPrompt, err := strconv.ParseFloat(model.Pricing.Prompt, 64)
73 if err != nil {
74 costPrompt = 0.0
75 }
76 pricing.CostPer1MIn = costPrompt * 1_000_000
77 costCompletion, err := strconv.ParseFloat(model.Pricing.Completion, 64)
78 if err != nil {
79 costCompletion = 0.0
80 }
81 pricing.CostPer1MOut = costCompletion * 1_000_000
82
83 costPromptCached, err := strconv.ParseFloat(model.Pricing.InputCacheWrite, 64)
84 if err != nil {
85 costPromptCached = 0.0
86 }
87 pricing.CostPer1MInCached = costPromptCached * 1_000_000
88 costCompletionCached, err := strconv.ParseFloat(model.Pricing.InputCacheRead, 64)
89 if err != nil {
90 costCompletionCached = 0.0
91 }
92 pricing.CostPer1MOutCached = costCompletionCached * 1_000_000
93 return pricing
94}
95
96func fetchOpenRouterModels() (*ModelsResponse, error) {
97 client := &http.Client{Timeout: 30 * time.Second}
98 req, _ := http.NewRequest("GET", "https://openrouter.ai/api/v1/models", nil)
99 req.Header.Set("User-Agent", "Crush-Client/1.0")
100 resp, err := client.Do(req)
101 if err != nil {
102 return nil, err
103 }
104 defer resp.Body.Close()
105 if resp.StatusCode != 200 {
106 body, _ := io.ReadAll(resp.Body)
107 return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
108 }
109 var mr ModelsResponse
110 if err := json.NewDecoder(resp.Body).Decode(&mr); err != nil {
111 return nil, err
112 }
113 return &mr, nil
114}
115
116// This is used to generate the openrouter.json config file
117func main() {
118 modelsResp, err := fetchOpenRouterModels()
119 if err != nil {
120 log.Fatal("Error fetching OpenRouter models:", err)
121 }
122
123 openRouterProvider := providers.Provider{
124 Name: "OpenRouter",
125 ID: "openrouter",
126 APIKey: "$OPENROUTER_API_KEY",
127 APIEndpoint: "https://openrouter.ai/api/v1",
128 Type: providers.ProviderTypeOpenAI,
129 DefaultModelID: "anthropic/claude-sonnet-4",
130 Models: []providers.Model{},
131 }
132
133 for _, model := range modelsResp.Data {
134 // skip nonβtext models or those without tools
135 if !slices.Contains(model.SupportedParams, "tools") ||
136 !slices.Contains(model.Architecture.InputModalities, "text") ||
137 !slices.Contains(model.Architecture.OutputModalities, "text") {
138 continue
139 }
140
141 pricing := getPricing(model)
142 canReason := slices.Contains(model.SupportedParams, "reasoning")
143 supportsImages := slices.Contains(model.Architecture.InputModalities, "image")
144
145 m := providers.Model{
146 ID: model.ID,
147 Name: model.Name,
148 CostPer1MIn: pricing.CostPer1MIn,
149 CostPer1MOut: pricing.CostPer1MOut,
150 CostPer1MInCached: pricing.CostPer1MInCached,
151 CostPer1MOutCached: pricing.CostPer1MOutCached,
152 ContextWindow: model.ContextLength,
153 CanReason: canReason,
154 SupportsImages: supportsImages,
155 }
156 if model.TopProvider.MaxCompletionTokens != nil {
157 m.DefaultMaxTokens = *model.TopProvider.MaxCompletionTokens
158 }
159 openRouterProvider.Models = append(openRouterProvider.Models, m)
160 }
161
162 // save the json in internal/providers/config/openrouter.json
163 data, err := json.MarshalIndent(openRouterProvider, "", " ")
164 if err != nil {
165 log.Fatal("Error marshaling OpenRouter provider:", err)
166 }
167 // write to file
168 err = os.WriteFile("internal/providers/configs/openrouter.json", data, 0o644)
169 if err != nil {
170 log.Fatal("Error writing OpenRouter provider config:", err)
171 }
172}