|
19 | 19 | (def canonical-providers |
20 | 20 | #{"glm" "claude-api" "claude-cli"}) |
21 | 21 |
|
| 22 | +(defn- getenv |
| 23 | + [k] |
| 24 | + (System/getenv k)) |
| 25 | + |
| 26 | +(defn- parse-provider-map |
| 27 | + [] |
| 28 | + (if-let [raw (getenv "NOUMENON_LLM_PROVIDERS_EDN")] |
| 29 | + (let [parsed (try |
| 30 | + (edn/read-string raw) |
| 31 | + (catch Exception e |
| 32 | + (throw (ex-info "NOUMENON_LLM_PROVIDERS_EDN contains invalid EDN" |
| 33 | + {:env-var "NOUMENON_LLM_PROVIDERS_EDN"} |
| 34 | + e))))] |
| 35 | + (when-not (map? parsed) |
| 36 | + (throw (ex-info "NOUMENON_LLM_PROVIDERS_EDN must be an EDN map" |
| 37 | + {:env-var "NOUMENON_LLM_PROVIDERS_EDN"}))) |
| 38 | + parsed) |
| 39 | + {})) |
| 40 | + |
| 41 | +(defn- configured-provider-names |
| 42 | + [] |
| 43 | + (->> (parse-provider-map) |
| 44 | + keys |
| 45 | + (filter keyword?) |
| 46 | + (remove #{:default-provider}) |
| 47 | + (map name) |
| 48 | + set)) |
| 49 | + |
| 50 | +(defn supported-provider-names |
| 51 | + [] |
| 52 | + (sort (into canonical-providers (configured-provider-names)))) |
| 53 | + |
| 54 | +(defn default-provider-name |
| 55 | + [] |
| 56 | + (let [providers-edn (parse-provider-map) |
| 57 | + env-default (getenv "NOUMENON_DEFAULT_PROVIDER") |
| 58 | + map-default (some-> (:default-provider providers-edn) name) |
| 59 | + selected (or env-default map-default default-provider)] |
| 60 | + (if ((set (supported-provider-names)) selected) |
| 61 | + selected |
| 62 | + (throw (ex-info (str "Default provider is not supported: " selected) |
| 63 | + {:provider selected :known (supported-provider-names)}))))) |
| 64 | + |
22 | 65 | (defn normalize-provider-name |
23 | 66 | "Normalize provider name string to canonical form. |
24 | 67 | Returns nil when the input is not a supported provider." |
25 | 68 | [provider] |
26 | 69 | (when provider |
27 | | - (let [normalized (get provider-aliases provider provider)] |
28 | | - (when (canonical-providers normalized) |
| 70 | + (let [normalized (get provider-aliases provider provider) |
| 71 | + providers (set (supported-provider-names))] |
| 72 | + (when (providers normalized) |
29 | 73 | normalized)))) |
30 | 74 |
|
31 | 75 | (defn provider->kw |
|
36 | 80 | normalized (normalize-provider-name provider-name)] |
37 | 81 | (when-not normalized |
38 | 82 | (throw (ex-info (str "Unrecognized provider: " provider-name |
39 | | - ". Known providers: " (str/join ", " (sort canonical-providers))) |
40 | | - {:provider provider-name :known (sort canonical-providers)}))) |
| 83 | + ". Known providers: " (str/join ", " (supported-provider-names))) |
| 84 | + {:provider provider-name :known (supported-provider-names)}))) |
41 | 85 | (keyword normalized))) |
42 | 86 |
|
43 | 87 | ;; --- Pricing --- |
|
271 | 315 |
|
272 | 316 | (def ^:private api-provider-config |
273 | 317 | {:glm {:env-var "NOUMENON_ZAI_TOKEN" |
274 | | - :base-url "https://api.z.ai/api/anthropic"} |
| 318 | + :base-url "https://api.z.ai/api/anthropic" |
| 319 | + :models-path "/v1/models"} |
275 | 320 | :claude-api {:env-var "ANTHROPIC_API_KEY" |
276 | | - :base-url "https://api.anthropic.com"}}) |
| 321 | + :base-url "https://api.anthropic.com" |
| 322 | + :models-path "/v1/models"}}) |
277 | 323 |
|
278 | 324 | (def ^:private runtime-modes #{"local" "service"}) |
279 | 325 |
|
280 | | -(defn- getenv |
281 | | - [k] |
282 | | - (System/getenv k)) |
283 | | - |
284 | 326 | (defn- runtime-mode |
285 | 327 | [] |
286 | 328 | (let [mode (or (getenv "NOUMENON_RUNTIME_MODE") "local")] |
|
346 | 388 |
|
347 | 389 | (defn- provider-map-config |
348 | 390 | [provider-kw] |
349 | | - (get (parse-edn-env "NOUMENON_LLM_PROVIDERS_EDN") provider-kw)) |
| 391 | + (get (parse-provider-map) provider-kw)) |
| 392 | + |
| 393 | +(defn provider-catalog |
| 394 | + [] |
| 395 | + (let [providers (parse-provider-map) |
| 396 | + names (supported-provider-names) |
| 397 | + default-p (default-provider-name)] |
| 398 | + {:default-provider default-p |
| 399 | + :providers (into {} |
| 400 | + (map (fn [provider-name] |
| 401 | + (let [provider-kw (keyword provider-name) |
| 402 | + configured (provider-map-config provider-kw) |
| 403 | + configured-models (:models configured) |
| 404 | + default-model (or (:default-model configured) |
| 405 | + (first configured-models) |
| 406 | + default-model-alias)] |
| 407 | + [provider-name {:default? (= provider-name default-p) |
| 408 | + :default-model default-model |
| 409 | + :models (or configured-models [])}]))) |
| 410 | + names)})) |
| 411 | + |
| 412 | +(defn- model-id-from-entry |
| 413 | + [entry] |
| 414 | + (or (:id entry) (:name entry) (:model entry))) |
| 415 | + |
| 416 | +(defn- parse-models-response |
| 417 | + [body] |
| 418 | + (let [parsed (json/read-str body :key-fn keyword) |
| 419 | + data (cond |
| 420 | + (vector? parsed) parsed |
| 421 | + (vector? (:data parsed)) (:data parsed) |
| 422 | + :else [])] |
| 423 | + (->> data |
| 424 | + (map model-id-from-entry) |
| 425 | + (filter string?) |
| 426 | + vec))) |
| 427 | + |
| 428 | +(defn- models-url |
| 429 | + [provider-kw base-url] |
| 430 | + (let [configured (provider-map-config provider-kw) |
| 431 | + path (or (:models-path configured) |
| 432 | + (:models-path (api-provider-config provider-kw)))] |
| 433 | + (when path |
| 434 | + (str (str/replace (str/trim base-url) #"/+$" "") path)))) |
| 435 | + |
| 436 | +(defn discover-provider-models |
| 437 | + ([provider] (discover-provider-models provider {})) |
| 438 | + ([provider {:keys [timeout-ms] :or {timeout-ms 15000}}] |
| 439 | + (let [provider-kw (provider->kw provider) |
| 440 | + configured (provider-map-config provider-kw) |
| 441 | + {:keys [env-var base-url]} (api-provider-config provider-kw) |
| 442 | + resolved-url (or (:base-url configured) base-url) |
| 443 | + api-key (or (:api-key configured) (read-env-var env-var)) |
| 444 | + url (models-url provider-kw resolved-url) |
| 445 | + fallback (vec (:models configured []))] |
| 446 | + (if-not url |
| 447 | + {:provider (name provider-kw) |
| 448 | + :default-model (or (:default-model configured) (first fallback) default-model-alias) |
| 449 | + :models fallback |
| 450 | + :source :config} |
| 451 | + (try |
| 452 | + (let [{:keys [status body error]} @(http/request {:url url |
| 453 | + :method :get |
| 454 | + :headers {"x-api-key" api-key |
| 455 | + "anthropic-version" "2023-06-01"} |
| 456 | + :timeout timeout-ms})] |
| 457 | + (if (or error (not= 200 status)) |
| 458 | + {:provider (name provider-kw) |
| 459 | + :default-model (or (:default-model configured) (first fallback) default-model-alias) |
| 460 | + :models fallback |
| 461 | + :source :config |
| 462 | + :warning (str "Model discovery unavailable (HTTP " status "), using configured fallback")} |
| 463 | + (let [models (parse-models-response body)] |
| 464 | + {:provider (name provider-kw) |
| 465 | + :default-model (or (:default-model configured) (first models) (first fallback) default-model-alias) |
| 466 | + :models (if (seq models) models fallback) |
| 467 | + :source (if (seq models) :api :config)}))) |
| 468 | + (catch Exception _ |
| 469 | + {:provider (name provider-kw) |
| 470 | + :default-model (or (:default-model configured) (first fallback) default-model-alias) |
| 471 | + :models fallback |
| 472 | + :source :config |
| 473 | + :warning "Model discovery failed, using configured fallback"})))))) |
| 474 | + |
| 475 | +(defn- provider-default-model |
| 476 | + [provider-kw] |
| 477 | + (:default-model (provider-map-config provider-kw))) |
| 478 | + |
| 479 | +(defn- provider-models |
| 480 | + [provider-kw] |
| 481 | + (:models (provider-map-config provider-kw))) |
| 482 | + |
| 483 | +(defn- normalize-model-name |
| 484 | + [model-name] |
| 485 | + (if (known-model-ids model-name) |
| 486 | + (model-alias->id model-name) |
| 487 | + model-name)) |
| 488 | + |
| 489 | +(defn- configured-model-set |
| 490 | + [provider-kw] |
| 491 | + (some->> (provider-models provider-kw) |
| 492 | + (map normalize-model-name) |
| 493 | + set)) |
| 494 | + |
| 495 | +(defn- resolve-model-id |
| 496 | + [provider-kw model] |
| 497 | + (let [configured (configured-model-set provider-kw) |
| 498 | + default-model (provider-default-model provider-kw) |
| 499 | + selected (or model |
| 500 | + default-model |
| 501 | + (first (provider-models provider-kw)) |
| 502 | + default-model-alias) |
| 503 | + resolved (normalize-model-name selected)] |
| 504 | + (when (and default-model (seq configured) |
| 505 | + (not (configured (normalize-model-name default-model)))) |
| 506 | + (throw (ex-info (str "Configured :default-model is not listed in :models for provider " (name provider-kw)) |
| 507 | + {:provider provider-kw :default-model default-model :allowed (sort configured)}))) |
| 508 | + (when (and (seq configured) (not (configured resolved))) |
| 509 | + (throw (ex-info (str "Model " resolved " is not configured for provider " (name provider-kw)) |
| 510 | + {:provider provider-kw :model resolved :allowed (sort configured)}))) |
| 511 | + resolved)) |
350 | 512 |
|
351 | 513 | (defn- normalize-base-url |
352 | 514 | [base-url] |
|
447 | 609 |
|
448 | 610 | (defn resolve-opts |
449 | 611 | "Resolve provider/model from option defaults. Returns map with :provider-kw |
450 | | - and :model-id — the canonical, validated identifiers." |
| 612 | + and :model-id — the canonical, validated identifiers." |
451 | 613 | [{:keys [provider model]}] |
452 | | - {:provider-kw (provider->kw (or provider default-provider)) |
453 | | - :model-id (model-alias->id (or model default-model-alias))}) |
| 614 | + (let [provider-kw (provider->kw (or provider (default-provider-name)))] |
| 615 | + {:provider-kw provider-kw |
| 616 | + :model-id (resolve-model-id provider-kw model)})) |
454 | 617 |
|
455 | 618 | (defn make-messages-fn-from-opts |
456 | 619 | "Build a messages-based invoke-fn from provider/model options. |
|
0 commit comments