Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
Extract non-core fragments using self-repair criterion:
- core-context -> symbolic-awareness (224 lines, fboundp guards in think())
- heartbeat generation -> symbolic-events (renamed events-start-heartbeat)
Rename 23 files for clarity and new naming scheme:
- 6 core: core-package, core-transport, core-pipeline,
core-perceive, core-reason, core-act
- 13 system: symbolic-*, neuro-*, embedding-*, channel-shell
- 4 gateway: channel-cli, channel-tui-*, channel-tui-state
Utility relocations:
- markdown-strip -> programming-markdown
- plist-keywords-normalize -> programming-lisp
- cognitive-tool-prompt -> programming-tools
- VAULT-MEMORY -> security-vault
- Merge *backend-registry* into *probabilistic-backends*
Split gateway-messaging into channel-telegram/channel-signal/
channel-discord/channel-slack (4 independent skills)
Delete dead system-model.lisp (16-line wrapper)
Document self-repair criterion in DESIGN_DECISIONS
Version bump: 0.4.3 -> 0.5.0
156 lines
7.4 KiB
Org Mode
156 lines
7.4 KiB
Org Mode
#+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-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"))))
|
|
#+end_src
|