|
6 | 6 | [eca.llm-providers.ollama :as llm-providers.ollama] |
7 | 7 | [eca.llm-util :as llm-util] |
8 | 8 | [eca.logger :as logger] |
9 | | - [eca.shared :refer [assoc-some] :as shared])) |
| 9 | + [eca.shared :refer [assoc-some] :as shared] |
| 10 | + [hato.client :as http])) |
10 | 11 |
|
11 | 12 | (set! *warn-on-reflection* true) |
12 | 13 |
|
|
77 | 78 | (and (llm-util/provider-api-url provider config) |
78 | 79 | (llm-util/provider-api-key provider (get-in db [:auth provider]) config))))) |
79 | 80 |
|
| 81 | +(def ^:private models-endpoint-path "/models") |
| 82 | + |
| 83 | +(defn ^:private fetch-compatible-models |
| 84 | + "Fetches models from an /models endpoint (both Anthropic and OpenAI). |
| 85 | + Returns a map of model-id -> {} (empty config, to be enriched later). |
| 86 | + On any error, logs a warning and returns nil." |
| 87 | + [{:keys [api-url api-key provider]}] |
| 88 | + (when api-url |
| 89 | + (let [url (str api-url models-endpoint-path) |
| 90 | + rid (llm-util/gen-rid) |
| 91 | + headers (cond-> {"Content-Type" "application/json"} |
| 92 | + api-key (assoc "Authorization" (str "Bearer " api-key)))] |
| 93 | + (try |
| 94 | + (llm-util/log-request logger-tag rid url nil headers) |
| 95 | + (let [{:keys [status body]} (http/get url |
| 96 | + {:headers headers |
| 97 | + :throw-exceptions? false |
| 98 | + :as :json |
| 99 | + :timeout 10000})] |
| 100 | + (if (= 200 status) |
| 101 | + (do |
| 102 | + (llm-util/log-response logger-tag rid "models" body) |
| 103 | + (let [models-data (:data body)] |
| 104 | + (when (seq models-data) |
| 105 | + (reduce |
| 106 | + (fn [acc model] |
| 107 | + (let [model-id (:id model)] |
| 108 | + (if model-id |
| 109 | + (assoc acc model-id {}) |
| 110 | + acc))) |
| 111 | + {} |
| 112 | + models-data)))) |
| 113 | + (logger/warn logger-tag |
| 114 | + (format "Provider '%s': /models endpoint returned status %s" |
| 115 | + provider status)))) |
| 116 | + (catch Exception e |
| 117 | + (logger/warn logger-tag |
| 118 | + (format "Provider '%s': Failed to fetch models from %s: %s" |
| 119 | + provider url (ex-message e)))))))) |
| 120 | + |
| 121 | +(defn ^:private provider-with-fetch-models? |
| 122 | + "Returns true if provider should fetch models dynamically (fetchModels = true)." |
| 123 | + [provider-config] |
| 124 | + (and (:api provider-config) |
| 125 | + (true? (:fetchModels provider-config)))) |
| 126 | + |
| 127 | +(defn ^:private fetch-dynamic-provider-models |
| 128 | + "For providers that support dynamic model discovery, |
| 129 | + attempts to fetch available models from the API. |
| 130 | + Returns a map of {provider-name -> {model-id -> model-config}}." |
| 131 | + [config db] |
| 132 | + (reduce |
| 133 | + (fn [acc [provider provider-config]] |
| 134 | + (if (provider-with-fetch-models? provider-config) |
| 135 | + (let [api-url (llm-util/provider-api-url provider config) |
| 136 | + [_auth-type api-key] (llm-util/provider-api-key provider |
| 137 | + (get-in db [:auth provider]) |
| 138 | + config) |
| 139 | + fetched-models (fetch-compatible-models |
| 140 | + {:api-url api-url |
| 141 | + :api-key api-key |
| 142 | + :provider provider})] |
| 143 | + (if (seq fetched-models) |
| 144 | + (do |
| 145 | + (logger/info logger-tag |
| 146 | + (format "Provider '%s': Discovered %d models from /models endpoint" |
| 147 | + provider (count fetched-models))) |
| 148 | + (assoc acc provider fetched-models)) |
| 149 | + acc)) |
| 150 | + acc)) |
| 151 | + {} |
| 152 | + (:providers config))) |
| 153 | + |
| 154 | +(defn ^:private build-model-capabilities |
| 155 | + "Build capabilities for a single model, looking up from known models database." |
| 156 | + [all-models provider model model-config] |
| 157 | + (let [real-model-name (or (:modelName model-config) model) |
| 158 | + full-real-model (str provider "/" real-model-name) |
| 159 | + full-model (str provider "/" model) |
| 160 | + model-capabilities (merge |
| 161 | + (or (get all-models full-real-model) |
| 162 | + ;; we guess the capabilities from |
| 163 | + ;; the first model with same name |
| 164 | + (when-let [found-full-model |
| 165 | + (->> (keys all-models) |
| 166 | + (filter #(or (= (shared/normalize-model-name (string/replace-first real-model-name |
| 167 | + #"(.+/)" |
| 168 | + "")) |
| 169 | + (shared/normalize-model-name (second (string/split % #"/" 2)))) |
| 170 | + (= (shared/normalize-model-name real-model-name) |
| 171 | + (shared/normalize-model-name (second (string/split % #"/" 2)))))) |
| 172 | + first)] |
| 173 | + (get all-models found-full-model)) |
| 174 | + {:tools true |
| 175 | + :reason? true |
| 176 | + :web-search false}) |
| 177 | + {:model-name real-model-name})] |
| 178 | + [full-model model-capabilities])) |
| 179 | + |
| 180 | +(defn ^:private merge-provider-models |
| 181 | + "Merges static config models with dynamically fetched models. |
| 182 | + Static config takes precedence (allows user overrides)." |
| 183 | + [static-models dynamic-models] |
| 184 | + (merge dynamic-models static-models)) |
| 185 | + |
80 | 186 | (defn sync-models! [db* config on-models-updated] |
81 | 187 | (let [all-models (all) |
82 | 188 | db @db* |
| 189 | + ;; Fetch dynamic models for providers that support it |
| 190 | + dynamic-provider-models (fetch-dynamic-provider-models config db) |
| 191 | + ;; Build all supported models from config + dynamic sources |
83 | 192 | all-supported-models (reduce |
84 | 193 | (fn [p [provider provider-config]] |
85 | | - (merge p |
86 | | - (reduce |
87 | | - (fn [m [model model-config]] |
88 | | - (let [real-model-name (or (:modelName model-config) model) |
89 | | - full-real-model (str provider "/" real-model-name) |
90 | | - full-model (str provider "/" model) |
91 | | - model-capabilities (merge |
92 | | - (or (get all-models full-real-model) |
93 | | - ;; we guess the capabilities from |
94 | | - ;; the first model with same name |
95 | | - (when-let [found-full-model (first (filter #(or (= (shared/normalize-model-name (string/replace-first real-model-name |
96 | | - #"(.+/)" |
97 | | - "")) |
98 | | - (shared/normalize-model-name (second (string/split % #"/" 2)))) |
99 | | - (= (shared/normalize-model-name real-model-name) |
100 | | - (shared/normalize-model-name (second (string/split % #"/" 2))))) |
101 | | - (keys all-models)))] |
102 | | - (get all-models found-full-model)) |
103 | | - {:tools true |
104 | | - :reason? true |
105 | | - :web-search false}) |
106 | | - {:model-name real-model-name})] |
107 | | - (assoc m full-model model-capabilities))) |
108 | | - {} |
109 | | - (:models provider-config)))) |
| 194 | + (let [static-models (:models provider-config) |
| 195 | + dynamic-models (get dynamic-provider-models provider) |
| 196 | + merged-models (merge-provider-models static-models dynamic-models)] |
| 197 | + (merge p |
| 198 | + (reduce |
| 199 | + (fn [m [model model-config]] |
| 200 | + (let [[full-model capabilities] (build-model-capabilities |
| 201 | + all-models provider model model-config)] |
| 202 | + (assoc m full-model capabilities))) |
| 203 | + {} |
| 204 | + merged-models)))) |
110 | 205 | {} |
111 | 206 | (:providers config)) |
112 | 207 | ollama-api-url (llm-util/provider-api-url "ollama" config) |
|
0 commit comments