From 4fed716f8ce894d5575dc174dc3ff632a3db8e40 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sun, 3 Aug 2025 17:37:34 +0300 Subject: [PATCH] Feat: openrouter fetch/update free model list --- llmapi/models.go | 49 ++++++++++++++++++++++++++++++++++++ llmapi/or.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 2 ++ todos.md | 3 +++ 4 files changed, 118 insertions(+) create mode 100644 llmapi/or.go diff --git a/llmapi/models.go b/llmapi/models.go index ea224e5..cc0bfea 100644 --- a/llmapi/models.go +++ b/llmapi/models.go @@ -95,3 +95,52 @@ func (b *Bot) ToPlayer() *models.Player { IsBot: true, } } + +type ORModel struct { + ID string `json:"id"` + CanonicalSlug string `json:"canonical_slug"` + HuggingFaceID string `json:"hugging_face_id"` + Name string `json:"name"` + Created int `json:"created"` + Description string `json:"description"` + ContextLength int `json:"context_length"` + Architecture struct { + Modality string `json:"modality"` + InputModalities []string `json:"input_modalities"` + OutputModalities []string `json:"output_modalities"` + Tokenizer string `json:"tokenizer"` + InstructType any `json:"instruct_type"` + } `json:"architecture"` + Pricing struct { + Prompt string `json:"prompt"` + Completion string `json:"completion"` + Request string `json:"request"` + Image string `json:"image"` + Audio string `json:"audio"` + WebSearch string `json:"web_search"` + InternalReasoning string `json:"internal_reasoning"` + } `json:"pricing,omitempty"` + TopProvider struct { + ContextLength int `json:"context_length"` + MaxCompletionTokens int `json:"max_completion_tokens"` + IsModerated bool `json:"is_moderated"` + } `json:"top_provider"` + PerRequestLimits any `json:"per_request_limits"` + SupportedParameters []string `json:"supported_parameters"` +} + +// https://openrouter.ai/api/v1/models +type ORModels struct { + Data []ORModel `json:"data"` +} + +func (orm *ORModels) ListFree() []string { + resp := []string{} + for _, model := range orm.Data { + if model.Pricing.Prompt == "0" && model.Pricing.Request == "0" && + model.Pricing.Completion == "0" { + resp = append(resp, model.ID) + } + } + return resp +} diff --git a/llmapi/or.go b/llmapi/or.go new file mode 100644 index 0000000..298e06a --- /dev/null +++ b/llmapi/or.go @@ -0,0 +1,64 @@ +package llmapi + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "time" +) + +var ( + ormodelsLink = "https://openrouter.ai/api/v1/models" + ORFreeModels = []string{ + "google/gemini-2.0-flash-exp:free", + "deepseek/deepseek-chat-v3-0324:free", + "mistralai/mistral-small-3.2-24b-instruct:free", + "qwen/qwen3-14b:free", + "google/gemma-3-27b-it:free", + "meta-llama/llama-3.3-70b-instruct:free", + } +) + +func ListORModels() ([]string, error) { + resp, err := http.Get(ormodelsLink) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + err := fmt.Errorf("failed to fetch or models; status: %s", resp.Status) + return nil, err + } + data := &ORModels{} + if err := json.NewDecoder(resp.Body).Decode(data); err != nil { + return nil, err + } + freeModels := data.ListFree() + return freeModels, nil +} + +func ORModelListUpdateTicker(ctx context.Context) { + ticker := time.NewTicker(time.Hour * 2) + freeModels, err := ListORModels() + slog.Info("updated free models list", "list", freeModels) + if err != nil { + slog.Error("failed to update free models list", "list", freeModels) + } + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + freeModels, err := ListORModels() + slog.Info("updated free models list", "list", freeModels) + if err != nil { + slog.Error("failed to update free models list", "list", freeModels) + // log + continue + } + ORFreeModels = freeModels + } + } +} diff --git a/main.go b/main.go index c453f13..49483ce 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "gralias/config" "gralias/crons" "gralias/handlers" + "gralias/llmapi" "gralias/repos" "gralias/telemetry" "log/slog" @@ -22,6 +23,7 @@ var cfg *config.Config func init() { cfg = config.LoadConfigOrDefault("") + go llmapi.ORModelListUpdateTicker(context.Background()) } // GzipFileServer serves pre-compressed .gz files if available diff --git a/todos.md b/todos.md index dca3e65..512a07a 100644 --- a/todos.md +++ b/todos.md @@ -36,6 +36,9 @@ - player stats: played games, lost, won, rating elo, opened opposite words, opened white words, opened black words. + - at the end of the game, all colors should be revealed; - tracing; +==== +- action history to bot (not json); basic stuff: what words were previously given as clue and guessed (maybe what team bot is on); +- automate getting list of free and non-thinking openrouter models; #### sse points - clue sse update;