Skip to content

Commit f17b09e

Browse files
committed
feat: enable/disable LLM providers
Add a toggle switch to each provider card. Disabled providers are excluded from the model list. Includes backend DB column, API changes, frontend UI, and Tailwind v4 fix for the Switch component styling.
1 parent 1359838 commit f17b09e

20 files changed

Lines changed: 166 additions & 6 deletions

File tree

backend/omni/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- `enabled` field on `ModelProvider`
13+
- `GET /api/models` now skips providers where `enabled=False`, so only active providers contribute models to the aggregated list.
14+
815
## [0.0.3] - 2026-04-28
916

1017
### Added

backend/omni/src/modai/modules/chat/__tests__/test_chat_llm_modules.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,7 @@ def _make_provider(
12271227
name=name,
12281228
base_url=base_url,
12291229
api_key=api_key,
1230+
enabled=True,
12301231
properties={},
12311232
created_at=None,
12321233
updated_at=None,
@@ -1240,6 +1241,7 @@ def _real_provider() -> ModelProviderResponse:
12401241
name="myopenai",
12411242
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
12421243
api_key=os.environ.get("UNIT_TEST_OPENAI_API_KEY", ""),
1244+
enabled=True,
12431245
properties={},
12441246
created_at=None,
12451247
updated_at=None,

backend/omni/src/modai/modules/model_provider/__tests__/test_central_model_provider_router.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def mock_provider_modules(self):
7373
name="OpenAI Production",
7474
base_url="https://api.openai.com/v1",
7575
api_key="sk-...",
76+
enabled=True,
7677
properties={},
7778
created_at="2024-01-15T10:30:00Z",
7879
updated_at="2024-01-15T10:30:00Z",
@@ -106,6 +107,7 @@ def mock_provider_modules(self):
106107
name="Local Ollama",
107108
base_url="http://localhost:11434",
108109
api_key="",
110+
enabled=True,
109111
properties={},
110112
created_at="2024-01-16T14:20:00Z",
111113
updated_at="2024-01-16T14:20:00Z",
@@ -203,6 +205,49 @@ def test_get_all_models_endpoint(self, test_client):
203205
assert "created" in model
204206
assert "owned_by" in model
205207

208+
def test_get_all_models_excludes_disabled_providers(self, mock_session_module):
209+
"""Test GET /models does not return models from disabled providers."""
210+
disabled_provider = ModelProviderResponse(
211+
id="openai-disabled",
212+
type="openai",
213+
name="Disabled Provider",
214+
base_url="https://api.disabled.com/v1",
215+
api_key="sk-disabled",
216+
enabled=False,
217+
properties={},
218+
created_at="2024-01-15T10:30:00Z",
219+
updated_at="2024-01-15T10:30:00Z",
220+
)
221+
disabled_models = ModelResponse(
222+
data=[
223+
{
224+
"id": "gpt-secret",
225+
"object": "model",
226+
"created": 1686935002,
227+
"owned_by": "openai",
228+
}
229+
]
230+
)
231+
disabled_module = DummyModelProviderModule(
232+
"openai", [disabled_provider], {"openai-disabled": disabled_models}
233+
)
234+
235+
dependencies = ModuleDependencies(
236+
{"disabled_provider": disabled_module, "session": mock_session_module}
237+
)
238+
router = CentralModelProviderRouter(dependencies, config={})
239+
app = FastAPI()
240+
app.include_router(router.router)
241+
client = TestClient(app)
242+
243+
response = client.get("/api/models")
244+
245+
assert response.status_code == 200
246+
data = response.json()
247+
model_ids = [m["id"] for m in data["data"]]
248+
assert not any("gpt-secret" in mid for mid in model_ids)
249+
assert data["data"] == []
250+
206251
def test_get_all_providers_with_pagination(self, test_client):
207252
"""Test GET /api/models/providers with pagination"""
208253
response = test_client.get("/api/models/providers?limit=1&offset=0")

backend/omni/src/modai/modules/model_provider/__tests__/test_model_provider.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def test_create_provider(
224224
name="NewProvider",
225225
url="https://api.new.com",
226226
properties=expected_properties,
227+
enabled=False,
227228
)
228229

229230
def test_create_provider_validation_error(
@@ -295,6 +296,7 @@ def test_update_provider(
295296
name="UpdatedProvider",
296297
url="https://api.updated.com",
297298
properties=expected_properties,
299+
enabled=None,
298300
)
299301

300302
def test_update_provider_not_found(
@@ -379,6 +381,7 @@ def test_complex_properties_handling(
379381
name="ComplexProvider",
380382
url="https://api.complex.com",
381383
properties=expected_properties,
384+
enabled=False,
382385
)
383386

384387
@pytest.mark.skipif(

backend/omni/src/modai/modules/model_provider/central_router.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,10 @@ async def get_all_models(self, request: Request) -> ModelsListResponse:
146146
request, limit=None, offset=None
147147
)
148148

149-
# For each provider, get its models
149+
# For each provider, get its models (only enabled ones)
150150
for provider in providers_response.providers:
151+
if not provider.enabled:
152+
continue
151153
try:
152154
models_response = await provider_module.get_models(
153155
request, provider.id

backend/omni/src/modai/modules/model_provider/module.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ModelProviderResponse(BaseModel):
1919
name: str
2020
base_url: str
2121
api_key: str
22+
enabled: bool
2223
properties: dict[str, Any]
2324
created_at: str | None
2425
updated_at: str | None
@@ -30,6 +31,7 @@ class ModelProviderCreateRequest(BaseModel):
3031
name: str
3132
base_url: str
3233
api_key: str
34+
enabled: bool | None = None
3335
properties: dict[str, Any] = {}
3436

3537

backend/omni/src/modai/modules/model_provider/openai_provider.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,12 @@ async def create_provider(
9595
properties = (provider_data.properties or {}).copy()
9696
properties["api_key"] = provider_data.api_key
9797

98-
# Create new provider
98+
# Create new provider (disabled by default)
9999
provider = await self.provider_store.add_provider(
100100
name=provider_data.name,
101101
url=provider_data.base_url,
102102
properties=properties,
103+
enabled=False,
103104
)
104105

105106
return self._create_provider_response(provider)
@@ -129,6 +130,7 @@ async def update_provider(
129130
name=provider_data.name,
130131
url=provider_data.base_url,
131132
properties=properties,
133+
enabled=provider_data.enabled,
132134
)
133135
if not provider:
134136
raise HTTPException(
@@ -208,6 +210,7 @@ def _create_provider_response(
208210
name=provider.name,
209211
base_url=provider.url,
210212
api_key=api_key,
213+
enabled=provider.enabled,
211214
properties=properties,
212215
created_at=provider.created_at.isoformat() if provider.created_at else None,
213216
updated_at=provider.updated_at.isoformat() if provider.updated_at else None,

backend/omni/src/modai/modules/model_provider_store/module.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ModelProvider:
2121
name: str
2222
url: str
2323
properties: dict[str, Any]
24+
enabled: bool = False
2425
created_at: datetime | None = None
2526
updated_at: datetime | None = None
2627

@@ -67,14 +68,15 @@ async def get_provider(self, provider_id: str) -> ModelProvider | None:
6768

6869
@abstractmethod
6970
async def add_provider(
70-
self, name: str, url: str, properties: dict[str, Any]
71+
self, name: str, url: str, properties: dict[str, Any], enabled: bool = False
7172
) -> ModelProvider:
7273
"""
7374
Adds a new model provider configuration.
7475
Args:
7576
name: Human-readable name for the provider
7677
url: API endpoint URL for the provider
7778
properties: Configuration properties specific to the provider
79+
enabled: Whether the provider is active (default: False)
7880
7981
Returns:
8082
Created ModelProvider object
@@ -91,6 +93,7 @@ async def update_provider(
9193
name: str,
9294
url: str,
9395
properties: dict[str, Any],
96+
enabled: bool | None = None,
9497
) -> ModelProvider | None:
9598
"""
9699
Updates an existing model provider configuration.
@@ -100,6 +103,7 @@ async def update_provider(
100103
name: New name for the provider (optional)
101104
url: New URL for the provider (optional)
102105
properties: New properties for the provider (optional)
106+
enabled: Whether the provider is active (None = keep current)
103107
104108
Returns:
105109
Updated ModelProvider object if found, None otherwise

backend/omni/src/modai/modules/model_provider_store/sql_model_provider_store.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import json
1010

1111
from sqlalchemy import (
12+
Boolean,
1213
create_engine,
1314
MetaData,
1415
Table,
@@ -58,6 +59,9 @@ def __init__(self, dependencies: ModuleDependencies, config: dict[str, Any]):
5859
Column("name", String(128), unique=True, index=True),
5960
Column("url", String(1000)),
6061
Column("properties", JSON),
62+
Column(
63+
"enabled", Boolean, default=False, nullable=False, server_default="0"
64+
),
6165
Column("created_at", DateTime, default=datetime.now),
6266
Column("updated_at", DateTime, default=datetime.now),
6367
)
@@ -85,6 +89,7 @@ def _row_to_provider(self, row) -> ModelProvider:
8589
name=row.name,
8690
url=row.url,
8791
properties=properties,
92+
enabled=bool(row.enabled) if row.enabled is not None else False,
8893
created_at=row.created_at,
8994
updated_at=row.updated_at,
9095
)
@@ -131,7 +136,7 @@ async def get_provider(self, provider_id: str) -> ModelProvider | None:
131136
return None
132137

133138
async def add_provider(
134-
self, name: str, url: str, properties: dict[str, Any]
139+
self, name: str, url: str, properties: dict[str, Any], enabled: bool = False
135140
) -> ModelProvider:
136141
provider_id = self._generate_provider_id()
137142
with self._get_session() as session:
@@ -146,6 +151,7 @@ async def add_provider(
146151
name=name.strip(),
147152
url=url.strip(),
148153
properties=properties,
154+
enabled=enabled,
149155
created_at=now,
150156
updated_at=now,
151157
)
@@ -159,6 +165,7 @@ async def add_provider(
159165
name=name.strip(),
160166
url=url.strip(),
161167
properties=properties,
168+
enabled=enabled,
162169
created_at=now,
163170
updated_at=now,
164171
)
@@ -169,6 +176,7 @@ async def update_provider(
169176
name: str,
170177
url: str,
171178
properties: dict[str, Any],
179+
enabled: bool | None = None,
172180
) -> ModelProvider | None:
173181
with self._get_session() as session:
174182
# Check if provider exists first
@@ -185,6 +193,7 @@ async def update_provider(
185193
properties = {}
186194

187195
now = datetime.now()
196+
new_enabled = enabled if enabled is not None else bool(existing_row.enabled)
188197

189198
# Update the provider
190199
update_stmt = (
@@ -194,6 +203,7 @@ async def update_provider(
194203
name=name.strip(),
195204
url=url.strip(),
196205
properties=properties,
206+
enabled=new_enabled,
197207
updated_at=now,
198208
)
199209
)
@@ -207,6 +217,7 @@ async def update_provider(
207217
name=name.strip(),
208218
url=url.strip(),
209219
properties=properties,
220+
enabled=new_enabled,
210221
created_at=existing_row.created_at,
211222
updated_at=now,
212223
)

frontend/omni/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- LLM provider management: each provider card now has a toggle switch to enable or disable the provider. Disabled providers are hidden from the model selection list.
13+
1014
### Changed
1115

1216
- The sidebar is now resizable.

0 commit comments

Comments
 (0)