main.go

  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}