Files
passepartout/org/system-model-explorer.org
Amr Gharbeia 908936d4d3 rename gateway-* → system-model-* + gateway-messaging, de-ollama, add system-model-explorer
- Rename gateway-provider → system-model-provider (generic :local provider, no hardcoded ollama)
- Rename gateway-llm → system-model (model-request dispatcher)
- Rename system-embedding-gateway → system-model-embedding
- Rename gateway-manager → gateway-messaging (public api renamed to messaging-*)
- Add system-model-explorer (model discovery via OpenRouter API, cached, per-slot recommendations)
- Fix skill loader export: replace prefix-matching with fbound/boundp-based export (20 skills now export)
- Add model-router to skill-loader exclusion list (loaded via CLI)
- De-ollama: remove hardcoded assumed-available patterns from provider pipeline
- Default cascade: cloud-only (openrouter, openai, groq, gemini, deepseek, nvidia, anthropic)
- Env example: add LOCAL_BASE_URL, fix cascade order
- All org files updated with architectural prose (literate programming)
2026-05-04 09:58:59 -04:00

6.3 KiB

SKILL: Model Explorer (org-skill-model-explorer.org)

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.

Implementation

Cache

(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)")

OpenRouter fetch

(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)))

Generic fetch with cache

(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))

List-free convenience

(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)))

Curated recommendations per slot

(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")))))

Tests

;; REPL-verified: 2026-05-04
(eval-when (:compile-toplevel :load-toplevel :execute)
  (ignore-errors (ql:quickload :fiveam :silent t)))

(defpackage :passepartout-system-model-explorer-tests
  (:use :cl :passepartout)
  (:export #:model-explorer-suite))

(in-package :passepartout-system-model-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
  "model-explorer-recommend should return 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
  "Each recommendation should have :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
  "Unknown slot should return fallback"
  (let ((recs (passepartout::model-explorer-recommend :unknown)))
    (fiveam:is (listp recs))
    (fiveam:is (>= (length recs) 1))))

(fiveam:test model-explorer-fetch-openrouter-count
  "OpenRouter API should return at least 300 models"
  (let ((models (passepartout::model-explorer-fetch :openrouter)))
    (if models
        (fiveam:is (>= (length models) 300))
        (fiveam:skip "API unreachable"))))