Files
passepartout/org/system-model-provider.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.8 KiB

SKILL: Unified LLM Backend (org-skill-unified-llm-backend.org)

Architectural Intent

system-model-provider is the universal LLM client. It speaks the OpenAI-compatible /v1/chat/completions protocol, which covers every modern provider — OpenRouter, OpenAI, Anthropic, Groq, Gemini, DeepSeek, NVIDIA NIM, plus any local engine (Ollama, vLLM, LM Studio, llama.cpp) when running behind an OpenAI-compatible adapter.

One function, eight (and counting) providers. The same JSON payload, the same response format, the same error handling. Adding a new provider is a one-line config entry: a keyword, a base URL, an API key env var name, and a default model.

Providers register themselves at boot. No API key? That provider doesn't register. No local URL set? The local entry stays dormant. Only the providers you actually configure appear in *probabilistic-backends* at runtime. The old code assumed Ollama was always available; this code requires an env var like everything else.

*provider-cascade* defaults to cloud-only (all providers except :local and :ollama). If you want a local fallback, set LOCAL_BASE_URL in your env and add :local to the PROVIDER_CASCADE list.

Implementation

Provider registry

(defparameter *provider-configs*
  '((:local      . (:base-url nil          :key-env nil           :url-env "LOCAL_BASE_URL"            :default-model "llama3"))
    (:openrouter . (:base-url "https://openrouter.ai/api/v1" :key-env "OPENROUTER_API_KEY" :default-model "openrouter/auto"))
    (:openai     . (:base-url "https://api.openai.com/v1"    :key-env "OPENAI_API_KEY"     :default-model "gpt-4o-mini"))
    (:anthropic  . (:base-url "https://api.anthropic.com/v1" :key-env "ANTHROPIC_API_KEY"  :default-model "claude-3-5-sonnet-20241022"))
    (:groq       . (:base-url "https://api.groq.com/openai/v1" :key-env "GROQ_API_KEY"     :default-model "llama-3.1-70b-versatile"))
    (:gemini     . (:base-url "https://generativelanguage.googleapis.com/v1beta/openai" :key-env "GEMINI_API_KEY" :default-model "gemini-2.0-flash"))
    (:deepseek   . (:base-url "https://api.deepseek.com/v1"  :key-env "DEEPSEEK_API_KEY"  :default-model "deepseek-chat"))
    (:nvidia     . (:base-url "https://integrate.api.nvidia.com/v1" :key-env "NVIDIA_API_KEY" :default-model "meta/llama-3.1-405b-instruct"))
    (:ollama     . (:base-url nil          :key-env nil           :url-env "OLLAMA_HOST"              :default-model "llama3"))))

Provider config lookup

(defun provider-config (provider)
  "Returns the configuration plist for a provider keyword."
  (cdr (assoc provider *provider-configs*)))

Availability check

(defun provider-available-p (provider)
  "Checks if a provider is configured. Checks API key or URL env vars."
  (let* ((config (provider-config provider))
         (key-env (getf config :key-env))
         (url-env (getf config :url-env))
         (base-url (getf config :base-url)))
    (cond (key-env (let ((key (uiop:getenv key-env))) (and key (> (length key) 0))))
          (url-env (let ((url (uiop:getenv url-env))) (and url (> (length url) 0))))
          (base-url t))))

Unified request execution

(defun provider-openai-request (prompt system-prompt &key model (provider :openrouter))
  "Executes a request against any OpenAI-compatible API endpoint."
  (let* ((config (provider-config provider))
         (base-url (getf config :base-url))
         (key-env (getf config :key-env))
         (url-env (getf config :url-env))
         (default-model (getf config :default-model))
         (api-key (when key-env (uiop:getenv key-env)))
         (model-id (or model default-model))
         (url (if url-env
                  (let ((host (uiop:getenv url-env)))
                    (if host
                        (format nil "http://~a/v1/chat/completions" host)
                        (format nil "~a/chat/completions" base-url)))
                  (format nil "~a/chat/completions" base-url)))
         (headers `(("Content-Type" . "application/json")
                    ,@(when api-key `(("Authorization" . ,(format nil "Bearer ~a" api-key))))
                    ,@(when (eq provider :openrouter)
                        `(("HTTP-Referer" . "https://github.com/amrgharbeia/passepartout")
                          ("X-Title" . "Passepartout")))))
         (body (cl-json:encode-json-to-string
                `((model . ,model-id)
                  (messages . (( (role . "system") (content . ,system-prompt) )
                               ( (role . "user") (content . ,prompt) )))))))
    (handler-case
        (let* ((response (dex:post url :headers headers :content body :connect-timeout 10 :read-timeout 60))
               (json (cl-json:decode-json-from-string response))
               (choices (cdr (assoc :choices json)))
               (first-choice (car choices))
               (message (cdr (assoc :message first-choice)))
               (content (cdr (assoc :content message))))
          (if content
              (list :status :success :content content)
              (list :status :error :message (format nil "~a: No content in response (~s)" provider json))))
      (error (c)
        (list :status :error :message (format nil "~a Failure: ~a" provider c))))))

Register all available providers

(defun provider-register-all ()
  "Scans environment variables and registers all available LLM backends."
  (dolist (entry *provider-configs*)
    (let ((provider (car entry)))
      (when (provider-available-p provider)
        (log-message "LLM BACKEND: Registering provider ~a" provider)
        (register-probabilistic-backend provider
          (lambda (prompt system-prompt &key model)
            (provider-openai-request prompt system-prompt :model model :provider provider)))))))

Initialize cascade

(defun provider-cascade-initialize ()
  "Reads PROVIDER_CASCADE from env and sets *provider-cascade*."
  (let ((cascade-str (uiop:getenv "PROVIDER_CASCADE")))
    (if cascade-str
        (setf *provider-cascade*
              (mapcar (lambda (s) (intern (string-upcase (string-trim '(#\Space) s)) :keyword))
                      (uiop:split-string cascade-str :separator '(#\,))))
        (setf *provider-cascade* (mapcar #'car (remove-if (lambda (e)
                                                             (member (car e) '(:local :ollama)))
                                                           *provider-configs*))))))

Boot registration

(provider-register-all)
(provider-cascade-initialize)

Skill registration

(defskill :passepartout-system-model-provider
  :priority 50
  :trigger (lambda (ctx) (declare (ignore ctx)) nil))