Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
- Restore (in-package :passepartout) to core-reason - Move *VAULT-MEMORY* back to core-skills - Fix ASDF and defstruct/defpackage ordering - Increase daemon timeout to 120s - Handshake: 0.5.0 Verified: daemon processes messages, TUI clean, gate trace works
110 lines
5.7 KiB
Common Lisp
110 lines
5.7 KiB
Common 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)")
|
|
|
|
(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")))))
|
|
|
|
(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).")))
|
|
|
|
;; 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
|
|
"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"))))
|