feat(ai): add model dropdown for Ollama and custom OpenAI-compatible providers#9692
Conversation
…providers - Add useProviderModels hook that fetches available models from /api/ai/models - Add backend endpoint GET /api/ai/models that proxies requests to OpenAI-compatible providers - Replace manual model text input with dropdown when models are available - Reset model selection when switching providers (only if model was selected from dropdown) - Avoids CORS issues by proxying through Marimo backend
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
for more information, see https://pre-commit.ci
dmadisetti
left a comment
There was a problem hiding this comment.
Thanks! I know ollama is a common option, but should be directly compatible with OpenAI api style inference. CC @Light2Dark for whether we want to support this. Hold on before hacking on this any more, but you'll also need to regen api schema (make codegen)
| LOGGER.warning( | ||
| "Could not fetch models from %s: %s", models_url, str(e) | ||
| ) | ||
| return StructResponse({"models": []}) |
There was a problem hiding this comment.
Do we just return nothing if we timeout?
There was a problem hiding this comment.
Good point on the timeout handling. I chose to return an empty list on error/timeout so the UI gracefully falls back to manual text input. with this way the settings dialog still works even if the local provider is temporarily unavailable. Happy to change this to return a proper HTTP error code if preferred.
|
Thanks for this @jens-koesling, I think another option was to call openai.clients list method. |
Thanks for reviewing! Regarding the httpx vs SDK approach: My question: does using the OpenAI SDK require however the main advantage is that this approach works with any OpenAI-compatible endpoint (Ollama, LM Studio, vLLM, etc.) without requiring additional dependencies. if you prefer the SDK approach I can change request method, no problem :) |
There was a problem hiding this comment.
Pull request overview
This PR adds automatic AI model discovery for Ollama and custom OpenAI-compatible providers by introducing a backend proxy endpoint and a frontend hook, then updating the AI model settings UI to show a dropdown when models are available.
Changes:
- Add
GET /api/ai/modelsbackend endpoint to fetch/v1/modelsfrom an OpenAI-compatiblebase_url. - Add
useProviderModelshook to fetch provider models via the backend (CORS-safe). - Update AI settings “Add model” form to use a dropdown when model options are available and reset selection appropriately on provider changes.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/openapi/src/api.ts | Adds the typed OpenAPI path definition for GET /api/ai/models. |
| packages/openapi/api.yaml | Adds the OpenAPI spec entry for GET /api/ai/models with base_url query param. |
| marimo/_server/api/endpoints/ai.py | Implements the new /api/ai/models proxy endpoint. |
| frontend/src/components/app-config/use-provider-models.ts | Introduces a hook to fetch model lists from the backend endpoint. |
| frontend/src/components/app-config/ai-config.tsx | Switches the model input to a dropdown when provider models are available and wires in the new hook. |
| try: | ||
| async with httpx.AsyncClient(timeout=5.0) as client: | ||
| response = await client.get(models_url) | ||
| response.raise_for_status() | ||
| data = response.json() | ||
| model_ids = sorted( | ||
| [m["id"] for m in data.get("data", []) if m.get("id")] | ||
| ) | ||
| return StructResponse(ProviderModelsResponse(models=model_ids)) | ||
| except Exception as e: | ||
| LOGGER.warning( | ||
| "Could not fetch models from %s: %s", models_url, str(e) | ||
| ) | ||
| return StructResponse(ProviderModelsResponse(models=[])) |
| base_url = request.query_params.get("base_url", "").rstrip("/") | ||
| if not base_url: | ||
| raise HTTPException( | ||
| status_code=HTTPStatus.BAD_REQUEST, | ||
| detail="base_url query parameter is required", | ||
| ) | ||
|
|
||
| # Normalize URL | ||
| if base_url.endswith("/v1"): | ||
| models_url = f"{base_url}/models" | ||
| else: | ||
| models_url = f"{base_url}/v1/models" | ||
|
|
||
| try: | ||
| async with httpx.AsyncClient(timeout=5.0) as client: | ||
| response = await client.get(models_url) | ||
| response.raise_for_status() | ||
| data = response.json() | ||
| model_ids = sorted( | ||
| [m["id"] for m in data.get("data", []) if m.get("id")] | ||
| ) | ||
| return StructResponse(ProviderModelsResponse(models=model_ids)) |
| @router.get("/models") | ||
| @requires("edit") | ||
| async def get_provider_models( | ||
| *, | ||
| request: Request, | ||
| ) -> Response: | ||
| """ | ||
| parameters: | ||
| - in: query | ||
| name: base_url | ||
| schema: | ||
| type: string | ||
| required: true | ||
| description: The base URL of the OpenAI-compatible provider | ||
| responses: | ||
| 200: | ||
| description: List of available models from the provider | ||
| content: | ||
| application/json: | ||
| schema: | ||
| type: object | ||
| properties: | ||
| models: | ||
| type: array | ||
| items: | ||
| type: string | ||
| """ | ||
|
|
||
| base_url = request.query_params.get("base_url", "").rstrip("/") | ||
| if not base_url: | ||
| raise HTTPException( | ||
| status_code=HTTPStatus.BAD_REQUEST, | ||
| detail="base_url query parameter is required", | ||
| ) | ||
|
|
| } finally { | ||
| setIsLoading(false); | ||
| } |
| const providerName = isCustomProvider ? customProviderName : provider; | ||
| const hasValidValues = providerName?.trim() && modelName?.trim(); | ||
|
|
||
| // Resolve base URL für den gewählten Provider |
|
|
||
| // Resolve base URL für den gewählten Provider | ||
| const resolvedProviderName = isCustomProvider ? customProviderName : provider; | ||
| const ollamaBaseUrl = form.getValues("ai.ollama.base_url"); |
| const providerName = isCustomProvider ? customProviderName : provider; | ||
| const hasValidValues = providerName?.trim() && modelName?.trim(); | ||
|
|
||
| // Resolve base URL für den gewählten Provider |
|
|
||
| // Resolve base URL für den gewählten Provider | ||
| const resolvedProviderName = isCustomProvider ? customProviderName : provider; | ||
| const ollamaBaseUrl = form.getValues("ai.ollama.base_url"); |
| from typing import TYPE_CHECKING, Literal | ||
|
|
||
| import httpx | ||
| from pydantic import BaseModel |
There was a problem hiding this comment.
Users may not have pydantic. We should not require this as an import, only as TYPE_CHECKING import if needed.
| useEffect(() => { | ||
| if (!baseUrl) { | ||
| setModels([]); | ||
| setError(null); | ||
| return; | ||
| } | ||
|
|
||
| const controller = new AbortController(); | ||
|
|
||
| const fetchModels = async () => { | ||
| setIsLoading(true); | ||
| setError(null); |
There was a problem hiding this comment.
consider using useAsyncData, it's a similar pattern
📝 Summary
When adding a custom AI model in the AI Models settings, users previously had to manually type the model name. This PR adds automatic model discovery for Ollama and custom OpenAI-compatible providers.
Changes
useProviderModelshook that fetches available models via the Marimo backendGET /api/ai/modelsthat proxies requests to OpenAI-compatible/v1/modelsendpointsTesting
Tested with Ollama and custom OpenAI-compatible providers.
Closes #5975
📋 Pre-Review Checklist
✅ Merge Checklist