feat(v0.2.0): unified OpenAI-compatible LLM backend
Replace Ollama-specific backend with unified org-skill-unified-llm-backend that speaks OpenAI API. Works with: - Local: Ollama (default), vLLM, LM Studio, llama.cpp - Cloud: OpenRouter, OpenAI, Anthropic, Groq, Gemini Providers auto-registered from env vars. No separate skills per provider. Cascade order configured via PROVIDER_CASCADE env var. Also fix .env loading path in loop (was .local/share, now .config matches wizard).
This commit is contained in:
@@ -105,7 +105,7 @@
|
|||||||
(defun main ()
|
(defun main ()
|
||||||
"Entry point for OpenCortex. Initializes the system and enters idle loop."
|
"Entry point for OpenCortex. Initializes the system and enters idle loop."
|
||||||
(let* ((home (uiop:getenv "HOME"))
|
(let* ((home (uiop:getenv "HOME"))
|
||||||
(env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
(env-file (uiop:merge-pathnames* ".config/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
||||||
(when (uiop:file-exists-p env-file)
|
(when (uiop:file-exists-p env-file)
|
||||||
(cl-dotenv:load-env env-file)))
|
(cl-dotenv:load-env env-file)))
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ The Metabolic Loop is the fundamental rhythm of OpenCortex: the continuous proce
|
|||||||
(defun main ()
|
(defun main ()
|
||||||
"Entry point for OpenCortex. Initializes the system and enters idle loop."
|
"Entry point for OpenCortex. Initializes the system and enters idle loop."
|
||||||
(let* ((home (uiop:getenv "HOME"))
|
(let* ((home (uiop:getenv "HOME"))
|
||||||
(env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
(env-file (uiop:merge-pathnames* ".config/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
||||||
(when (uiop:file-exists-p env-file)
|
(when (uiop:file-exists-p env-file)
|
||||||
(cl-dotenv:load-env env-file)))
|
(cl-dotenv:load-env env-file)))
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export MEMEX_DIR="${MEMEX_DIR:-$HOME/memex}"
|
|||||||
|
|
||||||
# Load environment variables from the standard config location
|
# Load environment variables from the standard config location
|
||||||
if [ -f "$OC_CONFIG_DIR/.env" ]; then
|
if [ -f "$OC_CONFIG_DIR/.env" ]; then
|
||||||
|
set -a
|
||||||
source "$OC_CONFIG_DIR/.env"
|
source "$OC_CONFIG_DIR/.env"
|
||||||
|
set +a
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Dependency Checker ---
|
# --- Dependency Checker ---
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
#+TITLE: SKILL: Llama Backend (org-skill-llama-backend.org)
|
|
||||||
#+AUTHOR: Agent
|
|
||||||
#+FILETAGS: :skill:llm:backend:ollama:
|
|
||||||
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-llama-backend.lisp
|
|
||||||
|
|
||||||
* Overview
|
|
||||||
The *Llama Backend* skill provides the actual implementation for calling local models via Ollama.
|
|
||||||
|
|
||||||
* Implementation
|
|
||||||
|
|
||||||
** Ollama API Call (ollama-call)
|
|
||||||
#+begin_src lisp
|
|
||||||
(defun ollama-call (prompt system-prompt &key (model "llama3"))
|
|
||||||
"Sends a request to the local Ollama API."
|
|
||||||
(let* ((host (or (uiop:getenv "OLLAMA_HOST") "localhost:11434"))
|
|
||||||
(url (format nil "http://~a/api/generate" host))
|
|
||||||
(payload (cl-json:encode-json-to-string
|
|
||||||
`((model . ,model)
|
|
||||||
(prompt . ,prompt)
|
|
||||||
(system . ,system-prompt)
|
|
||||||
(stream . nil)))))
|
|
||||||
(handler-case
|
|
||||||
(let ((response (dex:post url :content payload :headers '(("Content-Type" . "application/json")))))
|
|
||||||
(let ((data (cl-json:decode-json-from-string response)))
|
|
||||||
(list :status :success :content (getf data :response))))
|
|
||||||
(error (c)
|
|
||||||
(list :status :error :message (format nil "Ollama Failure: ~a" c))))))
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
** Skill Registration
|
|
||||||
#+begin_src lisp
|
|
||||||
(register-probabilistic-backend :ollama #'ollama-call)
|
|
||||||
|
|
||||||
(defskill :skill-llama-backend
|
|
||||||
:priority 50
|
|
||||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
|
||||||
#+end_src
|
|
||||||
106
skills/org-skill-unified-llm-backend.org
Normal file
106
skills/org-skill-unified-llm-backend.org
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#+TITLE: SKILL: Unified LLM Backend (org-skill-unified-llm-backend.org)
|
||||||
|
#+AUTHOR: Agent
|
||||||
|
#+FILETAGS: :skill:llm:backend:openai-compatible:
|
||||||
|
#+PROPERTY: header-args:lisp :tangle %%SKILLS_DIR%%/org-skill-unified-llm-backend.lisp
|
||||||
|
|
||||||
|
* Overview
|
||||||
|
The *Unified LLM Backend* provides a single OpenAI-compatible API client that works with:
|
||||||
|
- Local engines: Ollama, vLLM, LM Studio, llama.cpp (anything exposing /v1/chat/completions)
|
||||||
|
- Cloud providers: OpenRouter, OpenAI, Anthropic, Groq, Gemini (all OpenAI-compatible)
|
||||||
|
|
||||||
|
Providers are registered automatically based on available environment variables.
|
||||||
|
No separate skills per provider — just different base URLs and API keys.
|
||||||
|
|
||||||
|
* Implementation
|
||||||
|
|
||||||
|
** Provider Registry
|
||||||
|
#+begin_src lisp
|
||||||
|
(defparameter *unified-llm-providers*
|
||||||
|
'((:ollama . (:base-url nil :key-env nil :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"))))
|
||||||
|
|
||||||
|
(defun get-provider-config (provider)
|
||||||
|
"Returns the configuration plist for a provider keyword."
|
||||||
|
(cdr (assoc provider *unified-llm-providers*)))
|
||||||
|
|
||||||
|
(defun provider-available-p (provider)
|
||||||
|
"Checks if a provider is configured (has API key or is local Ollama)."
|
||||||
|
(let* ((config (get-provider-config provider))
|
||||||
|
(key-env (getf config :key-env))
|
||||||
|
(base-url (getf config :base-url)))
|
||||||
|
(cond ((eq provider :ollama) t) ; Ollama is always tried; failure is handled at call time
|
||||||
|
(key-env (let ((key (uiop:getenv key-env))) (and key (> (length key) 0))))
|
||||||
|
(base-url t))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Unified Request Execution
|
||||||
|
#+begin_src lisp
|
||||||
|
(defun execute-openai-compatible-request (prompt system-prompt &key model (provider :ollama))
|
||||||
|
"Executes a request against any OpenAI-compatible API endpoint."
|
||||||
|
(let* ((config (get-provider-config provider))
|
||||||
|
(base-url (getf config :base-url))
|
||||||
|
(key-env (getf config :key-env))
|
||||||
|
(default-model (getf config :default-model))
|
||||||
|
(api-key (when key-env (uiop:getenv key-env)))
|
||||||
|
(model-id (or model default-model))
|
||||||
|
(url (if (eq provider :ollama)
|
||||||
|
(format nil "http://~a/v1/chat/completions" (or (uiop:getenv "OLLAMA_HOST") "localhost:11434"))
|
||||||
|
(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/opencortex")
|
||||||
|
("X-Title" . "OpenCortex")))))
|
||||||
|
(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))))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Dynamic Backend Registration
|
||||||
|
#+begin_src lisp
|
||||||
|
(defun register-available-llm-backends ()
|
||||||
|
"Scans environment variables and registers all available LLM backends."
|
||||||
|
(dolist (entry *unified-llm-providers*)
|
||||||
|
(let ((provider (car entry)))
|
||||||
|
(when (provider-available-p provider)
|
||||||
|
(harness-log "LLM BACKEND: Registering provider ~a" provider)
|
||||||
|
(register-probabilistic-backend provider
|
||||||
|
(lambda (prompt system-prompt &key model)
|
||||||
|
(execute-openai-compatible-request prompt system-prompt :model model :provider provider)))))))
|
||||||
|
|
||||||
|
(defun initialize-provider-cascade ()
|
||||||
|
"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 *unified-llm-providers*)))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Skill Registration
|
||||||
|
#+begin_src lisp
|
||||||
|
(register-available-llm-backends)
|
||||||
|
(initialize-provider-cascade)
|
||||||
|
|
||||||
|
(defskill :skill-unified-llm-backend
|
||||||
|
:priority 50
|
||||||
|
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||||
|
#+end_src
|
||||||
Reference in New Issue
Block a user