provider: add mlx as a first-class OpenAI-compatible provider

ober

501f886000f7e0080a67b459b1a563334f894729

diff --git a/src/jcode/core/models.ss b/src/jcode/core/models.ss
index 6a1a997..67ebf2e 100644
--- a/src/jcode/core/models.ss
+++ b/src/jcode/core/models.ss
@@ -22,7 +22,7 @@
 ;; ---- Provider metadata ----
 
 (def (all-providers)
-  '("anthropic" "openai" "openrouter" "deepseek" "google" "ollama"
+  '("anthropic" "openai" "openrouter" "deepseek" "google" "ollama" "mlx"
     "xai" "groq" "mistral" "together" "cerebras" "perplexity"))
 
 (def (provider-display-name p)
@@ -33,6 +33,7 @@
     ((deepseek)    "DeepSeek")
     ((google)      "Google")
     ((ollama)      "Ollama (local)")
+    ((mlx)         "MLX (local)")
     ((xai)         "xAI (Grok)")
     ((groq)        "Groq")
     ((mistral)     "Mistral")
@@ -49,6 +50,7 @@
     ((deepseek)    "deepseek-chat")
     ((google)      "gemini-2.5-flash")
     ((ollama)      "llama3.2")
+    ((mlx)         "/Users/user/mine/jerboa-lora/jerboa-mlx-4bit-v2")
     ((xai)         "grok-3-mini")
     ((groq)        "llama-3.3-70b-versatile")
     ((mistral)     "mistral-large-latest")
@@ -76,6 +78,7 @@
     ((google)      google-models)
     ((openrouter)  openrouter-models)
     ((ollama)      ollama-models)
+    ((mlx)         mlx-models)
     ((xai)         xai-models)
     ((groq)        groq-models)
     ((mistral)     mistral-models)
@@ -209,6 +212,9 @@
     ("codellama"       . "Code Llama")
     ("gemma2"          . "Gemma 2")))
 
+(def mlx-models
+  '(("/Users/user/mine/jerboa-lora/jerboa-mlx-4bit-v2" . "Jerboa Qwen3 30B (MLX 4-bit)")))
+
 (def xai-models
   '(("grok-3"          . "Grok 3")
     ("grok-3-mini"     . "Grok 3 Mini")
diff --git a/src/jcode/provider/provider.ss b/src/jcode/provider/provider.ss
index 8ffa48e..39a3607 100644
--- a/src/jcode/provider/provider.ss
+++ b/src/jcode/provider/provider.ss
@@ -90,6 +90,7 @@
     ((openrouter)  "https://openrouter.ai/api/v1")
     ((deepseek)    "https://api.deepseek.com/v1")
     ((ollama)      "http://localhost:11434/v1")
+    ((mlx)         "http://127.0.0.1:8080/v1")
     ((xai)         "https://api.x.ai/v1")
     ((groq)        "https://api.groq.com/openai/v1")
     ((mistral)     "https://api.mistral.ai/v1")
@@ -100,7 +101,8 @@
 
 (def (provider-chat provider messages tools)
   (unless (or (provider-api-key provider)
-              (equal? (provider-name provider) "ollama"))
+              (equal? (provider-name provider) "ollama")
+              (equal? (provider-name provider) "mlx"))
     (error 'provider-chat
       (format "No API key configured for provider '~a'. Set the appropriate env var or add it to jcode.json."
         (provider-name provider))))
@@ -116,6 +118,7 @@
         ((anthropic)  (anthropic-chat provider messages tools))
         ((google)     (google-chat provider messages tools))
         ((ollama)     (ollama-chat provider messages tools))
+        ((mlx)        (openai-chat provider messages tools))
         (else (error 'provider-chat "Unknown provider" (provider-name provider)))))))
 
 (def (provider-stream provider messages tools callback)
@@ -1022,7 +1025,8 @@
 ;; Returns (values content-str tool-call-list usage-alist).
 (def (provider-stream-chat provider messages tools token-cb)
   (unless (or (provider-api-key provider)
-              (equal? (provider-name provider) "ollama"))
+              (equal? (provider-name provider) "ollama")
+              (equal? (provider-name provider) "mlx"))
     (error 'provider-stream-chat
       (format "No API key configured for provider '~a'. Set the appropriate env var or add it to jcode.json."
         (provider-name provider))))
@@ -1031,7 +1035,7 @@
       (model    . ,(provider-model provider))
       (messages . ,(length messages))))
   (case (string->symbol (provider-name provider))
-    ((openai openrouter deepseek ollama xai groq mistral together cerebras perplexity)
+    ((openai openrouter deepseek ollama mlx xai groq mistral together cerebras perplexity)
      (openai-stream-chat  provider messages tools token-cb))
     ((anthropic) (anthropic-stream-chat provider messages tools token-cb))
     ((google)
@@ -1265,7 +1269,8 @@
 ;; Raises on network/API errors. Ollama does not require a key.
 (def (provider-list-models provider)
   (unless (or (provider-api-key provider)
-              (equal? (provider-name provider) "ollama"))
+              (equal? (provider-name provider) "ollama")
+              (equal? (provider-name provider) "mlx"))
     (error 'provider-list-models
       (format "No API key for provider '~a'" (provider-name provider))))
   (log-info logger "list-models"
@@ -1275,6 +1280,7 @@
     ((anthropic)                  (anthropic-list-models provider))
     ((google)                     (google-list-models provider))
     ((ollama)                     (ollama-list-models provider))
+    ((mlx)                        (ollama-list-models provider))
     (else
      (error 'provider-list-models
        (format "Unknown provider '~a'" (provider-name provider))))))