feat: add first-class oMLX provider via Esperanto profile (#1164)

Expose Esperanto's built-in omlx OpenAICompatibleProfile in Settings with
Ollama-style UX (default http://localhost:11435/v1, optional API key,
language+embedding discovery). No openai-compatible remapping or
OPENAI_COMPATIBLE_* env mirroring.

Fixes #1048

Co-authored-by: Luis Novo <lfnovo@gmail.com>
This commit is contained in:
Gautam Diwan 2026-07-19 14:02:14 -04:00 • committed by GitHub
parent d25cbb1b19
commit ef9bc43b9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 431 additions and 6 deletions

View file

@ -50,6 +50,10 @@ SURREAL_DATABASE=open_notebook
# Ollama endpoint (if running locally)
# OLLAMA_API_BASE=http://localhost:11434
# oMLX (Apple Silicon) — prefer Settings → API Keys; port 11435 avoids SurrealDB on 8000
# OMLX_API_BASE=http://localhost:11435/v1
# OMLX_API_KEY= # optional; only if oMLX was started with --api-key
# Content processing
# CHUNK_SIZE=1500
# CHUNK_OVERLAP=150

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **OpenRouter now supports text-to-speech and speech-to-text**, not just LLMs and embeddings (esperanto 2.25.0). The provider's modalities were extended so the Settings UI offers TTS/STT when registering OpenRouter models. Because OpenRouter's `/models` listing does not reliably tag audio models, discovery seeds the working defaults esperanto ships — `microsoft/mai-voice-2` (TTS) and `openai/whisper-1` + `openai/whisper-large-v3` (STT) — alongside the API-discovered language models; any other `vendor/model` id can be added manually. Note that `microsoft/mai-voice-2` uses Microsoft neural voice names (e.g. `en-US-AvaNeural`), not OpenAI's `alloy`/`nova` set (#987)
- Newer ElevenLabs models are now offered in model discovery: `eleven_v3`, `eleven_flash_v2_5` and `eleven_flash_v2` for text-to-speech, and `scribe_v2` for speech-to-text — alongside the existing models. Esperanto passes the `model_id` straight through to ElevenLabs, so no provider changes were needed (#1167)
- **First-class oMLX provider** in Settings → API Keys: native Esperanto `omlx` profile identity (language + embedding), default base URL `http://localhost:11435/v1` (avoids SurrealDB on 8000), optional API key, connection test and `/v1/models` discovery. No remapping to `openai-compatible` and no `OPENAI_COMPATIBLE_*` env mirroring. Docs under `docs/5-CONFIGURATION/omlx.md` (#1048; upstream lfnovo/esperanto#228)
- **Four new AI providers** (surfaced by esperanto 2.25.1) are now in the provider matrix. **Cohere** (`COHERE_API_KEY`) adds language and embedding models via Cohere's native v2 API (`command-*` chat, `embed-*`); model discovery is bespoke (esperanto's `AIFactory.get_provider_models`) since Cohere is not OpenAI-compatible, and Cohere reranking is intentionally left out of scope (tracked at #1087). **PayPerQ / PPQ** (`PPQ_API_KEY`, `https://api.ppq.ai/v1`) is a multi-modality OpenAI-compatible gateway offering language, embedding, speech-to-text and text-to-speech. **Novita** (`NOVITA_API_KEY`, `https://api.novita.ai/openai`) is an OpenAI-compatible LLM gateway. PPQ and Novita auto-discover their models through the standard `/models` endpoint. In addition, the existing **Deepgram** provider now also supports **speech-to-text** (Nova/Whisper transcription models, e.g. `nova-3`) alongside its Aura text-to-speech voices. All four are configured from Settings → Models exactly like the existing providers (#1170)
### Fixed

View file

@ -229,6 +229,7 @@ Thanks to the [Esperanto](https://github.com/lfnovo/esperanto) library, we suppo
| Google (GenAI) | ✅ | ✅ | ✅ | ✅ |
| Vertex AI | ✅ | ✅ | ❌ | ✅ |
| Ollama | ✅ | ✅ | ❌ | ❌ |
| oMLX | ✅ | ✅ | ❌ | ❌ |
| Perplexity | ✅ | ❌ | ❌ | ❌ |
| ElevenLabs | ❌ | ❌ | ✅ | ✅ |
| Deepgram | ❌ | ❌ | ✅ | ✅ |
@ -245,7 +246,7 @@ Thanks to the [Esperanto](https://github.com/lfnovo/esperanto) library, we suppo
| PayPerQ (PPQ) | ✅ | ✅ | ✅ | ✅ |
| OpenAI Compatible* | ✅ | ✅ | ✅ | ✅ |
*Supports LM Studio and any OpenAI-compatible endpoint
*Supports LM Studio and any OpenAI-compatible endpoint. Prefer the native **oMLX** provider for [oMLX](https://omlx.ai/) (Apple Silicon); see [docs/5-CONFIGURATION/omlx.md](docs/5-CONFIGURATION/omlx.md).
## ✨ Key Features

View file

@ -119,6 +119,15 @@ def create_credential_from_env(provider: str) -> Credential:
modalities=modalities,
base_url=os.environ.get("OLLAMA_API_BASE"),
)
elif provider == "omlx":
api_key = os.environ.get("OMLX_API_KEY")
return Credential(
name=name,
provider=provider,
modalities=modalities,
api_key=SecretStr(api_key) if api_key else None,
base_url=os.environ.get("OMLX_API_BASE"),
)
elif provider == "vertex":
return Credential(
name=name,
@ -261,6 +270,15 @@ async def test_credential(credential_id: str) -> dict:
)
return {"provider": provider, "success": success, "message": message}
if provider == "omlx":
# Esperanto oMLX profile default; port 11435 avoids SurrealDB on 8000.
base_url = config.get("base_url") or "http://localhost:11435/v1"
api_key = config.get("api_key")
success, message = await _test_openai_compatible_connection(
base_url, api_key
)
return {"provider": provider, "success": success, "message": message}
if provider == "azure":
success, message = await _test_azure_connection(
endpoint=config.get("endpoint"),
@ -461,6 +479,33 @@ async def discover_with_config(provider: str, config: dict) -> List[dict]:
logger.warning(f"Failed to discover openai_compatible models: {e}")
return []
if provider == "omlx":
omlx_url = base_url or "http://localhost:11435/v1"
try:
target = await prepare_pinned_http_target(
models_endpoint(omlx_url), "omlx"
)
headers = dict(target.headers)
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
async with httpx.AsyncClient() as client:
response = await client.get(
target.url,
headers=headers,
timeout=30.0,
extensions=target.extensions,
)
response.raise_for_status()
data = response.json()
return [
{"name": m.get("id", ""), "provider": "omlx"}
for m in data.get("data", [])
if m.get("id")
]
except Exception as e:
logger.warning(f"Failed to discover omlx models: {e}")
return []
if provider == "azure":
endpoint = config.get("endpoint")
api_version = config.get("api_version", "2024-10-21")

View file

@ -574,6 +574,7 @@ SupportedProvider = Literal[
"elevenlabs",
"deepgram",
"ollama",
"omlx",
"azure",
"vertex",
"openai_compatible",

View file

@ -405,6 +405,7 @@ async def get_provider_availability():
"elevenlabs": "ELEVENLABS_API_KEY",
"deepgram": "DEEPGRAM_API_KEY",
"ollama": "OLLAMA_API_BASE",
"omlx": "OMLX_API_BASE",
"dashscope": "DASHSCOPE_API_KEY",
"minimax": "MINIMAX_API_KEY",
"novita": "NOVITA_API_KEY",

View file

@ -102,6 +102,7 @@ Navigation: Settings → API Keys
| Provider | Required Fields | Notes |
|----------|-----------------|-------|
| Ollama | Base URL | Typically `http://localhost:11434` or `http://ollama:11434` |
| oMLX | Base URL | Default `http://localhost:11435/v1` (avoids SurrealDB on 8000); API key optional |
### Enterprise
@ -206,6 +207,15 @@ For OpenAI, Anthropic, Google, Groq, Mistral, DeepSeek, xAI, OpenRouter:
Ollama allows localhost and private IPs since it runs locally.
### oMLX (Apple Silicon)
1. Add credential with provider **oMLX**
2. Base URL defaults to `http://localhost:11435/v1` (do not use port `8000` — that conflicts with SurrealDB)
3. API key is optional
4. Test connection → discover and register language/embedding models
See [oMLX Setup](../5-CONFIGURATION/omlx.md) for install and Docker host networking notes.
### Azure OpenAI
1. Add credential with provider **Azure OpenAI**

View file

@ -106,6 +106,16 @@ Open Notebook supports 17+ AI providers. This guide helps you **choose the right
→ [Setup Guide](../5-CONFIGURATION/ai-providers.md#ollama-recommended-for-local)
**oMLX (Apple Silicon)**
- Cost: Free (electricity only)
- Speed: Fast on M-series GPUs (MLX)
- Quality: Good (bring-your-own MLX models)
- Setup: 10 minutes
- Best for: macOS Apple Silicon, private local inference
- Privacy: 100% local; default URL `http://localhost:11435/v1` (avoids SurrealDB on 8000)
→ [Setup Guide](../5-CONFIGURATION/omlx.md)
**LM Studio (Alternative)**
- Cost: Free (electricity only)
- Speed: Depends on hardware
@ -144,6 +154,7 @@ Open Notebook supports 17+ AI providers. This guide helps you **choose the right
| **Novita** | Fast | $ | Varies | Low | 5 min | Varies |
| **PayPerQ (PPQ)** | Varies | Varies | Varies | Low | 5 min | Varies |
| **Ollama** | Slow-Medium | Free | Good | Max | 10 min | Varies |
| **oMLX** | Fast (MLX) | Free | Good | Max | 10 min | Varies |
| **LM Studio** | Slow-Medium | Free | Good | Max | 15 min | Varies |
| **Azure** | Very Fast | $$ | Excellent | High | 10 min | 128K |
@ -163,6 +174,9 @@ Open Notebook supports 17+ AI providers. This guide helps you **choose the right
### I want privacy/offline
→ **Ollama** — Free, local, private
### I have an Apple Silicon Mac
→ **oMLX** — Free, local MLX inference ([setup](../5-CONFIGURATION/omlx.md))
### I want a GUI (not CLI)
→ **LM Studio** — Desktop app

View file

@ -494,6 +494,32 @@ CPU-only:
---
### oMLX (Apple Silicon)
**Cost:** Free (electricity only)
**Requirements:** Apple Silicon Mac. oMLX runs on the host (not in Linux containers).
**Setup oMLX:**
1. Install from [oMLX](https://omlx.ai/) / [jundot/omlx](https://github.com/jundot/omlx)
2. Run on port **11435** (oMLX’s default `8000` conflicts with SurrealDB):
```bash
OMLX_PORT=11435 omlx serve
```
3. Load models in the oMLX admin UI
**Configure in Open Notebook:**
1. Go to **Settings** → **API Keys**
2. Click **Add Credential**
3. Select provider: **oMLX**
4. Base URL defaults to `http://localhost:11435/v1` (use `http://host.docker.internal:11435/v1` if Open Notebook is in Docker)
5. API key is optional (only if you started oMLX with `--api-key`)
6. Click **Save**, then **Test Connection** → **Discover Models** → **Register Models**
See [oMLX Setup Guide](omlx.md) for port conflict details and troubleshooting.
---
### LM Studio (Local Alternative)
**Cost:** Free

View file

@ -298,6 +298,8 @@ If you have these variables configured from a previous installation, click the *
| `DEEPSEEK_API_KEY` | DeepSeek | Settings → API Keys → Add DeepSeek Credential |
| `XAI_API_KEY` | xAI | Settings → API Keys → Add xAI Credential |
| `OLLAMA_API_BASE` | Ollama | Settings → API Keys → Add Ollama Credential |
| `OMLX_API_BASE` | oMLX | Settings → API Keys → Add oMLX Credential |
| `OMLX_API_KEY` | oMLX | Optional; only if oMLX was started with `--api-key` |
| `OPENROUTER_API_KEY` | OpenRouter | Settings → API Keys → Add OpenRouter Credential |
| `OPENROUTER_BASE_URL` | OpenRouter | Configure in OpenRouter credential |
| `VOYAGE_API_KEY` | Voyage AI | Settings → API Keys → Add Voyage AI Credential |

View file

@ -29,14 +29,15 @@ Setup: Get API key → Add credential in Settings UI → Done
### Option 2: Local (Free & Private)
- **Ollama** (open-source models, on your machine)
- **oMLX** (Apple Silicon / MLX inference server)
→ Go to **[Ollama Setup](ollama.md)**
→ Go to **[Ollama Setup](ollama.md)** or **[oMLX Setup](omlx.md)**
### Option 3: OpenAI-Compatible
- **LM Studio** (local)
- **Custom endpoints**
→ Go to **[OpenAI-Compatible Guide](openai-compatible.md)**
→ Go to **[OpenAI-Compatible Guide](openai-compatible.md)** (for oMLX, prefer the native [oMLX](omlx.md) provider)
---
@ -212,6 +213,9 @@ OPEN_NOTEBOOK_ENCRYPTION_KEY=my-secret-key
### [Ollama](ollama.md)
- Setting up and pointing to an Ollama server
### [oMLX](omlx.md)
- Apple Silicon MLX server (default `http://localhost:11435/v1`, SurrealDB port note)
- Downloading models
- Using embedding

View file

@ -0,0 +1,125 @@
# oMLX Setup Guide
[oMLX](https://omlx.ai/) is a macOS-native inference server for Apple Silicon. It runs [MLX](https://opensource.apple.com/projects/mlx/) models locally and exposes an **OpenAI-compatible API** at `/v1`, so Open Notebook can use it for language and embedding models without sending data to the cloud.
Open Notebook treats **oMLX** as a first-class provider in Settings → API Keys. Identity comes from Esperanto’s built-in `omlx` [OpenAI-compatible profile](https://github.com/lfnovo/esperanto/issues/228) (`AIFactory.create_language("omlx", ...)` / `create_embedding("omlx", ...)`). There is no remapping to the generic `openai-compatible` provider and no `OPENAI_COMPATIBLE_*` env mirroring.
## Why Choose oMLX?
- **Apple Silicon optimized** — MLX inference on M-series Macs
- **OpenAI-compatible** — drop-in `/v1/chat/completions` and `/v1/embeddings`
- **Private** — models and prompts stay on your machine
- **Optional API key** — protect the local server with `--api-key` if needed
## Requirements
- Apple Silicon Mac (M1 or later)
- macOS 15+ (Sequoia) recommended
- oMLX installed and running on the **host** (oMLX cannot run inside Linux containers)
## Install oMLX
**DMG (recommended):** download from [oMLX Releases](https://github.com/jundot/omlx/releases), drag to Applications, and launch.
**Homebrew:**
```bash
brew tap jundot/omlx
brew install omlx
brew services start omlx
```
**CLI:**
```bash
omlx serve
# or with a custom port (recommended — see Port Conflict below):
OMLX_PORT=11435 omlx serve
```
Admin UI (when using oMLX’s own default port): `http://localhost:8000/admin`
## Port Conflict with SurrealDB
oMLX and SurrealDB both default to **port 8000**. Open Notebook’s database uses `8000`, so running oMLX on the default port will collide.
**Recommended:** run oMLX on **11435** (same idea as Ollama’s 11434):
```bash
OMLX_PORT=11435 omlx serve
```
Or set the port in oMLX’s admin settings / `~/.omlx/settings.json`.
Then use this base URL in Open Notebook:
```text
http://localhost:11435/v1
```
> Always include the `/v1` suffix — oMLX’s OpenAI API lives under `/v1` (e.g. `GET /v1/models`).
## Configure Open Notebook
1. Go to **Settings** → **API Keys**
2. Click **Add Credential** → select **oMLX**
3. Set **Base URL** to `http://localhost:11435/v1` (prefilled by default)
4. Optionally set an **API key** if you started oMLX with `--api-key`
5. **Save** → **Test Connection** → **Discover Models** → register language and embedding models
### Legacy env vars (optional fallback)
```bash
# Prefer Settings UI; env is for migration / headless setups
export OMLX_API_BASE=http://localhost:11435/v1
# Optional, only if oMLX was started with --api-key
# export OMLX_API_KEY=your-key
```
These match Esperanto’s profile env names (`OMLX_API_BASE`, `OMLX_API_KEY`).
## Network Notes (Docker)
oMLX itself must run on the Mac host. If Open Notebook runs in Docker:
| Setup | Base URL |
|-------|----------|
| Both on host | `http://localhost:11435/v1` |
| Open Notebook in Docker, oMLX on host | `http://host.docker.internal:11435/v1` |
Ensure oMLX listens on an interface Docker can reach.
## Modalities
| Modality | Supported |
|----------|-----------|
| Language | ✅ |
| Embedding | ✅ |
| Speech-to-text | ❌ |
| Text-to-speech | ❌ |
For local STT/TTS, see [Local STT](local-stt.md) and [Local TTS](local-tts.md) (e.g. Speaches via OpenAI-Compatible).
## Troubleshooting
| Symptom | What to check |
|---------|----------------|
| Connection refused | Is oMLX running? Correct port? |
| Wrong port / SurrealDB errors | Don’t use `8000` for oMLX — use `11435` (or another free port) |
| 404 on `/models` | Base URL must end with `/v1` |
| 401 Unauthorized | API key mismatch with oMLX `--api-key` |
| No models listed | Load/download models in the oMLX admin UI first |
| Docker can’t reach host | Use `host.docker.internal` and confirm oMLX bind address |
Quick connectivity check:
```bash
curl http://localhost:11435/v1/models
```
## Related
- [Ollama Setup](ollama.md) — another local provider (native Ollama API)
- [OpenAI-Compatible](openai-compatible.md) — generic OpenAI API servers (LM Studio, vLLM, …)
- [AI Providers](ai-providers.md) — all provider options
- [API Configuration](../3-USER-GUIDE/api-configuration.md) — credentials UI walkthrough

View file

@ -26,6 +26,7 @@ Open Notebook can connect to any server using this format.
| **Text Generation WebUI** | Full-featured local inference | https://github.com/oobabooga/text-generation-webui |
| **vLLM** | High-performance serving | https://github.com/vllm-project/vllm |
| **Ollama** | Simple local models | (Use native Ollama provider instead) |
| **oMLX** | Apple Silicon / MLX | (Use native [oMLX](omlx.md) provider instead) |
| **LocalAI** | Local AI inference | https://github.com/mudler/LocalAI |
| **llama.cpp server** | Lightweight inference | https://github.com/ggerganov/llama.cpp |
@ -403,3 +404,4 @@ Use OpenAI-compatible when:
- **[Local STT Setup](local-stt.md)** - Speech-to-text with Speaches
- **[AI Providers](ai-providers.md)** - All provider options
- **[Ollama Setup](ollama.md)** - Native Ollama integration
- **[oMLX Setup](omlx.md)** - Native oMLX (Apple Silicon) integration

View file

@ -16,6 +16,9 @@ import { Credential, CreateCredentialRequest } from '@/lib/api/credentials'
import { buildCredentialUpdatePayload } from '@/lib/credential-update-payload'
import { useProviders } from '@/lib/hooks/use-providers'
/** Default oMLX OpenAI base URL (port 11435 avoids SurrealDB on 8000). */
const OMLX_DEFAULT_BASE_URL = 'http://localhost:11435/v1'
interface CredentialFormDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
@ -43,8 +46,10 @@ export function CredentialFormDialog({
const isVertex = provider === 'vertex'
const isOllama = provider === 'ollama'
const isOmlx = provider === 'omlx'
const isOpenAICompatible = provider === 'openai_compatible'
const requiresApiKey = !isVertex && !isOllama && !isOpenAICompatible
// oMLX is a local no-auth endpoint by default (optional --api-key).
const requiresApiKey = !isVertex && !isOllama && !isOmlx && !isOpenAICompatible
const [name, setName] = useState('')
const [apiKey, setApiKey] = useState('')
@ -69,7 +74,8 @@ export function CredentialFormDialog({
setModalities(credential.modalities || [])
} else {
setName('')
setBaseUrl('')
// Prefill oMLX default (port 11435 avoids SurrealDB on 8000).
setBaseUrl(isOmlx ? OMLX_DEFAULT_BASE_URL : '')
setApiKey('')
setProject('')
setLocation('')
@ -234,7 +240,13 @@ export function CredentialFormDialog({
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder={isOllama ? 'http://localhost:11434' : 'https://api.example.com/v1'}
placeholder={
isOllama
? 'http://localhost:11434'
: isOmlx
? OMLX_DEFAULT_BASE_URL
: 'https://api.example.com/v1'
}
disabled={isSubmitting}
/>
<p className="text-xs text-muted-foreground">{t('apiKeys.baseUrlOverrideHint')}</p>

View file

@ -64,6 +64,12 @@ PROVIDER_CONFIG = {
"ollama": {
"env_var": "OLLAMA_API_BASE",
},
# oMLX: Esperanto built-in profile (OMLX_API_KEY / OMLX_API_BASE).
# Optional API key; base_url from the credential sets OMLX_API_BASE via
# _provision_simple_provider. No OPENAI_COMPATIBLE_* mirroring.
"omlx": {
"env_var": "OMLX_API_KEY",
},
"dashscope": {
"env_var": "DASHSCOPE_API_KEY",
},

View file

@ -723,6 +723,71 @@ async def discover_openai_compatible_models() -> List[DiscoveredModel]:
return models
_OMLX_DEFAULT_BASE_URL = "http://localhost:11435/v1"
async def discover_omlx_models() -> List[DiscoveredModel]:
"""
Fetch available models from a local oMLX server (OpenAI-compatible /v1/models).
Uses Credential config when present, else OMLX_API_BASE / OMLX_API_KEY.
Default base URL is http://localhost:11435/v1 (avoids SurrealDB on 8000).
API key is optional (oMLX is a local no-auth endpoint by default).
"""
api_key = None
base_url = None
try:
credentials = await Credential.get_by_provider("omlx")
if credentials:
cred = credentials[0]
config = cred.to_esperanto_config()
api_key = config.get("api_key")
base_url = (config.get("base_url") or "").rstrip("/")
except Exception as e:
logger.warning(f"Failed to read omlx config from Credential: {e}")
if not api_key:
api_key = os.environ.get("OMLX_API_KEY")
if not base_url:
base_url = os.environ.get("OMLX_API_BASE", _OMLX_DEFAULT_BASE_URL).rstrip("/")
models: List[DiscoveredModel] = []
try:
target = await prepare_pinned_http_target(_models_endpoint(base_url), "omlx")
headers = dict(target.headers)
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
async with httpx.AsyncClient() as client:
response = await client.get(
target.url,
headers=headers,
timeout=30.0,
extensions=target.extensions,
)
response.raise_for_status()
data = response.json()
for model in data.get("data", []):
model_id = model.get("id", "")
if model_id:
model_type = classify_model_type(model_id, "openai")
models.append(
DiscoveredModel(
name=model_id,
provider="omlx",
model_type=model_type,
)
)
except httpx.HTTPStatusError as e:
logger.warning(f"Failed to discover omlx models: HTTP {e.response.status_code}")
except Exception as e:
logger.warning(f"Failed to discover omlx models: {e}")
return models
# =============================================================================
# Main Discovery Functions
# =============================================================================
@ -733,6 +798,7 @@ PROVIDER_DISCOVERY_FUNCTIONS = {
"anthropic": discover_anthropic_models,
"google": discover_google_models,
"ollama": discover_ollama_models,
"omlx": discover_omlx_models,
"groq": discover_groq_models,
"mistral": discover_mistral_models,
"deepseek": discover_deepseek_models,

View file

@ -221,6 +221,18 @@ _PROVIDER_SPECS: Tuple[ProviderSpec, ...] = (
required_env=("OLLAMA_API_BASE",),
test_model=None, # Dynamic - uses first available model
),
ProviderSpec(
name="omlx",
display_name="oMLX",
modalities=("language", "embedding"),
required_env=("OMLX_API_BASE",),
optional_env=("OMLX_API_KEY",),
test_model=None, # Dynamic - uses first available model via /v1/models
docs_url="https://github.com/lfnovo/open-notebook/blob/main/docs/5-CONFIGURATION/omlx.md",
# No openai_compat_discovery_url: base URL is user-supplied (default
# http://localhost:11435/v1) and API key is optional — discovery is
# handled like openai_compatible, not the fixed-URL OPENAI_COMPAT table.
),
ProviderSpec(
name="azure",
display_name="Azure OpenAI",

View file

@ -38,6 +38,7 @@ KNOWN_GOOD_PROVIDERS = [
"elevenlabs",
"deepgram",
"ollama",
"omlx",
"azure",
"vertex",
"openai_compatible",

View file

@ -261,6 +261,97 @@ class TestCredentialModelDiscovery:
assert captured["extensions"] == {"sni_hostname": "llm-gateway.example.com"}
class TestOmlxDiscovery:
"""oMLX uses OpenAI-compatible /v1/models with an optional API key."""
@pytest.mark.asyncio
async def test_omlx_discovery_defaults_base_url_and_pins(self, monkeypatch):
from open_notebook.utils.url_validation import PinnedHttpTarget
captured = {}
pinned_calls = []
class FakeAsyncClient:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return None
async def get(self, url, headers=None, timeout=None, extensions=None):
captured["url"] = url
captured["headers"] = headers
return httpx.Response(
200,
json={"data": [{"id": "mlx-model"}]},
request=httpx.Request("GET", url, headers=headers or {}),
)
async def fake_prepare_pinned(url, provider):
pinned_calls.append((url, provider))
return PinnedHttpTarget(url=url)
monkeypatch.setattr(credentials_service.httpx, "AsyncClient", FakeAsyncClient)
monkeypatch.setattr(
credentials_service, "prepare_pinned_http_target", fake_prepare_pinned
)
models = await credentials_service.discover_with_config("omlx", {})
assert models == [{"name": "mlx-model", "provider": "omlx"}]
assert pinned_calls == [("http://localhost:11435/v1/models", "omlx")]
assert captured["url"] == "http://localhost:11435/v1/models"
assert "Authorization" not in (captured["headers"] or {})
@pytest.mark.asyncio
async def test_omlx_discovery_sends_optional_api_key(self, monkeypatch):
from open_notebook.utils.url_validation import PinnedHttpTarget
captured = {}
class FakeAsyncClient:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return None
async def get(self, url, headers=None, timeout=None, extensions=None):
captured["headers"] = headers
return httpx.Response(
200,
json={"data": [{"id": "mlx-model"}]},
request=httpx.Request("GET", url, headers=headers or {}),
)
async def fake_prepare_pinned(url, provider):
return PinnedHttpTarget(url=url)
monkeypatch.setattr(credentials_service.httpx, "AsyncClient", FakeAsyncClient)
monkeypatch.setattr(
credentials_service, "prepare_pinned_http_target", fake_prepare_pinned
)
await credentials_service.discover_with_config(
"omlx",
{
"api_key": "secret",
"base_url": "http://127.0.0.1:11435/v1",
},
)
assert captured["headers"]["Authorization"] == "Bearer secret"
def test_omlx_registry_modalities_and_env(self):
from api.credentials_service import PROVIDER_ENV_CONFIG, PROVIDER_MODALITIES
from open_notebook.ai.connection_tester import TEST_MODELS
assert PROVIDER_MODALITIES["omlx"] == ["language", "embedding"]
assert PROVIDER_ENV_CONFIG["omlx"]["required"] == ["OMLX_API_BASE"]
assert PROVIDER_ENV_CONFIG["omlx"]["optional"] == ["OMLX_API_KEY"]
assert TEST_MODELS["omlx"] == (None, "language")
class TestCredentialNumCtx:
"""Tests for the Ollama num_ctx override threaded into esperanto config."""

View file

@ -66,6 +66,7 @@ class TestOpenAICompatTable:
"anthropic",
"google",
"ollama",
"omlx",
"groq",
"mistral",
"deepseek",