mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-10-19 01:08:07 +00:00
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:
parent
d25cbb1b19
commit
ef9bc43b9a
20 changed files with 431 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -574,6 +574,7 @@ SupportedProvider = Literal[
|
|||
"elevenlabs",
|
||||
"deepgram",
|
||||
"ollama",
|
||||
"omlx",
|
||||
"azure",
|
||||
"vertex",
|
||||
"openai_compatible",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
125
docs/5-CONFIGURATION/omlx.md
Normal file
125
docs/5-CONFIGURATION/omlx.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ KNOWN_GOOD_PROVIDERS = [
|
|||
"elevenlabs",
|
||||
"deepgram",
|
||||
"ollama",
|
||||
"omlx",
|
||||
"azure",
|
||||
"vertex",
|
||||
"openai_compatible",
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class TestOpenAICompatTable:
|
|||
"anthropic",
|
||||
"google",
|
||||
"ollama",
|
||||
"omlx",
|
||||
"groq",
|
||||
"mistral",
|
||||
"deepseek",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue