#+TITLE: SKILL: Model Explorer (org-skill-model-explorer.org) #+AUTHOR: Agent #+FILETAGS: :skill:model:explorer:discovery: #+PROPERTY: header-args:lisp :tangle ../lisp/neuro-explorer.lisp * Architectural Intent ~system-model-explorer~ answers two questions the config screen needs: "What models does my provider offer?" and "Which one should I use for this task?" It opens a thin pipe to OpenRouter's /api/v1/models endpoint (no API key needed for the model list), parses the JSON into a uniform set of plists, and caches the result. The TUI's model dropdowns and recommendation cards all read from this cache. Recommended models are curated per task slot — code generation needs different capabilities than casual chat or background summarization. The recommendations are not hardcoded provider hooks; they're hand-picked from the OpenRouter free tier as a sensible default. Users can override via the TUI config screen, which replaces the picked model IDs into their cascade. ** Contract 1. (model-explorer-recommend slot): returns a list of plists with ~:id~ and ~:name~ for the given task slot (~:code~, ~:chat~, ~:plan~, ~:background~). Unknown slots return a fallback list. 2. (model-explorer-fetch provider): fetches the model list from the provider's API and caches it. Returns nil on failure. * Implementation ** Cache #+begin_src lisp (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)") #+end_src ** OpenRouter fetch #+begin_src lisp (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))) #+end_src ** Generic fetch with cache #+begin_src lisp (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)) #+end_src ** List-free convenience #+begin_src lisp (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))) #+end_src ** Curated recommendations per slot #+begin_src lisp (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"))))) #+end_src ** Slot descriptions (for TUI config display) ;; REPL-verified: 2026-05-04 #+begin_src lisp (defvar *slot-descriptions* '((:code . "Code generation, refactoring, debugging. Needs strong reasoning and large context.\nRecommend: Qwen3 Coder (free, 35B active) or Laguna M.1 (coding agent).") (:chat . "Casual conversation, Q&A, creative writing. Prefer balanced quality, low latency.\nRecommend: Llama 3.3 70B (strong generalist) or Gemma 4 31B (thinking mode).") (:plan . "Strategic planning, architecture design, complex multi-step reasoning.\nRecommend: Owl Alpha (free, tool use, 1M ctx) or Hermes 3 405B (strongest free reasoning).") (:background . "Heartbeat summaries, delegation responses, tool output filtering. Must be small + fast.\nRecommend: Llama 3.2 3B (131K ctx, fast) or LFM 2.5 1.2B (edge-ready)."))) #+end_src * Tests #+begin_src lisp ;; REPL-verified: 2026-05-04 (eval-when (:compile-toplevel :load-toplevel :execute) (ignore-errors (ql:quickload :fiveam :silent t))) (defpackage :passepartout-neuro-explorer-tests (:use :cl :passepartout) (:export #:model-explorer-suite)) (in-package :passepartout-neuro-explorer-tests) (fiveam:def-suite model-explorer-suite :description "Tests for the model explorer skill") (fiveam:in-suite model-explorer-suite) (fiveam:test model-explorer-recommend-slots "Contract 1: recommend returns models for all standard slots." (dolist (slot '(:code :chat :plan :background)) (let ((recs (passepartout::model-explorer-recommend slot))) (fiveam:is (listp recs)) (fiveam:is (>= (length recs) 1))))) (fiveam:test model-explorer-recommend-format "Contract 1: each recommendation has :id and :name." (dolist (rec (passepartout::model-explorer-recommend :chat)) (fiveam:is (getf rec :id)) (fiveam:is (getf rec :name)))) (fiveam:test model-explorer-recommend-unknown-slot "Contract 1: unknown slot returns fallback list." (let ((recs (passepartout::model-explorer-recommend :unknown))) (fiveam:is (listp recs)) (fiveam:is (>= (length recs) 1)))) (fiveam:test model-explorer-fetch-openrouter-count "Contract 2: OpenRouter API returns at least 300 models." (let ((models (passepartout::model-explorer-fetch :openrouter))) (if models (fiveam:is (>= (length models) 300)) (fiveam:skip "API unreachable")))) #+end_src