(in-package :passepartout) (defvar *model-cache* (make-hash-table :test 'equal) "Cache: provider keyword -> (timestamp . model-list)") (defvar *model-cache-ttl* 300 "Cache TTL in seconds (default 5 min)") (defun model-explorer-fetch-openrouter () "Query OpenRouter /api/v1/models and return parsed model list." (handler-case (let* ((raw (dex:get "https://openrouter.ai/api/v1/models" :connect-timeout 10 :read-timeout 20)) (json (cl-json:decode-json-from-string raw)) (data (cdr (assoc :data json)))) (mapcar (lambda (m) (let ((pricing (cdr (assoc :pricing m)))) (list :id (cdr (assoc :id m)) :name (cdr (assoc :name m)) :context (cdr (assoc :context_length m)) :free (and pricing (string= "0" (cdr (assoc :prompt pricing))) (string= "0" (cdr (assoc :completion pricing))))))) data)) (error (c) (log-message "MODEL-EXPLORER: OpenRouter API error: ~a" c) nil))) (defun model-explorer-fetch (provider) "Fetch available models for PROVIDER. Returns list of (:id :name :context :free) plists." (let ((cached (gethash provider *model-cache*))) (when (and cached (< (- (get-universal-time) (car cached)) *model-cache-ttl*)) (return-from model-explorer-fetch (cdr cached)))) (let ((models (case provider (:openrouter (model-explorer-fetch-openrouter)) (t nil)))) (when models (setf (gethash provider *model-cache*) (cons (get-universal-time) models))) models)) (defun model-explorer-list-free () "Return all free models from cache or fetch." (remove-if-not (lambda (m) (getf m :free)) (model-explorer-fetch :openrouter))) (defun model-explorer-recommend (slot) "Return recommended models for SLOT (:code, :chat, :plan, :background)." (case slot (:code '((:id "qwen/qwen3-coder:free" :name "Qwen3 Coder 480B" :context 262000 :free t :note "Top-tier code MoE, 35B active") (:id "poolside/laguna-m.1:free" :name "Laguna M.1" :context 131072 :free t :note "Flagship coding agent") (:id "openai/gpt-oss-120b:free" :name "gpt-oss-120b" :context 131072 :free t :note "117B MoE open-weight coding"))) (:plan '((:id "openrouter/owl-alpha" :name "Owl Alpha" :context 1048756 :free t :note "Agentic, tool use, reasoning") (:id "nousresearch/hermes-3-llama-3.1-405b:free" :name "Hermes 3 405B" :context 131072 :free t :note "405B generalist, strong planning") (:id "minimax/minimax-m2.5:free" :name "MiniMax M2.5" :context 196608 :free t :note "SOTA productivity, long context"))) (:chat '((:id "meta-llama/llama-3.3-70b-instruct:free" :name "Llama 3.3 70B" :context 65536 :free t :note "Strong multilingual generalist") (:id "google/gemma-4-31b-it:free" :name "Gemma 4 31B" :context 262144 :free t :note "Dense 31B, thinking mode, long context") (:id "mistralai/mistral-nemo:free" :name "Mistral Nemo" :context 32768 :free t :note "Fast, good for casual conversation"))) (:background '((:id "meta-llama/llama-3.2-3b-instruct:free" :name "Llama 3.2 3B" :context 131072 :free t :note "Small, fast, efficient") (:id "liquid/lfm-2.5-1.2b-instruct:free" :name "LFM 2.5 1.2B" :context 32768 :free t :note "Ultra-compact, edge-ready"))) (t '((:id "meta-llama/llama-3.3-70b-instruct:free" :name "Llama 3.3 70B" :context 65536 :free t :note "Safe fallback")))))