Skip to content

Commit 7e86c6c

Browse files
Add include/exclude model filters
1 parent 2e102e3 commit 7e86c6c

File tree

16 files changed

+698
-23
lines changed

16 files changed

+698
-23
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Add routing group tables.
2+
3+
Revision ID: 006
4+
Revises: 005
5+
Create Date: 2026-01-05 00:00:00.000000
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = "006"
16+
down_revision: Union[str, None] = "005"
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
op.create_table(
23+
"routing_groups",
24+
sa.Column("id", sa.Integer, primary_key=True),
25+
sa.Column("name", sa.String, nullable=False, unique=True, index=True),
26+
sa.Column("description", sa.Text, nullable=True),
27+
sa.Column("capabilities", sa.Text, nullable=True),
28+
sa.Column("created_at", sa.DateTime, nullable=False),
29+
sa.Column("updated_at", sa.DateTime, nullable=False),
30+
)
31+
32+
op.create_table(
33+
"routing_targets",
34+
sa.Column("id", sa.Integer, primary_key=True),
35+
sa.Column("group_id", sa.Integer, sa.ForeignKey("routing_groups.id", ondelete="CASCADE"), nullable=False),
36+
sa.Column("provider_id", sa.Integer, sa.ForeignKey("providers.id", ondelete="CASCADE"), nullable=False),
37+
sa.Column("model_id", sa.String, nullable=False),
38+
sa.Column("weight", sa.Integer, nullable=False, server_default="1"),
39+
sa.Column("priority", sa.Integer, nullable=False, server_default="0"),
40+
sa.Column("enabled", sa.Boolean, nullable=False, server_default="1"),
41+
sa.Column("created_at", sa.DateTime, nullable=False),
42+
sa.Column("updated_at", sa.DateTime, nullable=False),
43+
sa.UniqueConstraint("group_id", "provider_id", "model_id", name="uq_routing_target"),
44+
)
45+
op.create_index("ix_routing_targets_group_id", "routing_targets", ["group_id"])
46+
op.create_index("ix_routing_targets_provider_id", "routing_targets", ["provider_id"])
47+
op.create_index("ix_routing_targets_priority", "routing_targets", ["priority"])
48+
49+
op.create_table(
50+
"routing_provider_limits",
51+
sa.Column("id", sa.Integer, primary_key=True),
52+
sa.Column("group_id", sa.Integer, sa.ForeignKey("routing_groups.id", ondelete="CASCADE"), nullable=False),
53+
sa.Column("provider_id", sa.Integer, sa.ForeignKey("providers.id", ondelete="CASCADE"), nullable=False),
54+
sa.Column("max_requests_per_hour", sa.Integer, nullable=True),
55+
sa.Column("created_at", sa.DateTime, nullable=False),
56+
sa.Column("updated_at", sa.DateTime, nullable=False),
57+
sa.UniqueConstraint("group_id", "provider_id", name="uq_routing_provider_limit"),
58+
)
59+
op.create_index("ix_routing_provider_limits_group_id", "routing_provider_limits", ["group_id"])
60+
op.create_index("ix_routing_provider_limits_provider_id", "routing_provider_limits", ["provider_id"])
61+
62+
63+
def downgrade() -> None:
64+
op.drop_index("ix_routing_provider_limits_provider_id", table_name="routing_provider_limits")
65+
op.drop_index("ix_routing_provider_limits_group_id", table_name="routing_provider_limits")
66+
op.drop_table("routing_provider_limits")
67+
68+
op.drop_index("ix_routing_targets_priority", table_name="routing_targets")
69+
op.drop_index("ix_routing_targets_provider_id", table_name="routing_targets")
70+
op.drop_index("ix_routing_targets_group_id", table_name="routing_targets")
71+
op.drop_table("routing_targets")
72+
73+
op.drop_table("routing_groups")

backend/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Backend sync worker service."""
22

3-
__version__ = "0.6.20"
3+
__version__ = "0.6.21"

backend/provider_sync.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@
1111
logger = logging.getLogger(__name__)
1212

1313

14+
def _parse_filter_terms(raw: str | None) -> list[str]:
15+
"""Split comma-separated filter terms into normalized lowercase tokens."""
16+
if not raw:
17+
return []
18+
return [term.strip().lower() for term in raw.split(",") if term.strip()]
19+
20+
21+
def _matches_include_exclude(model_id: str, include_terms: list[str], exclude_terms: list[str]) -> bool:
22+
"""Return True if model_id matches include terms and not excluded."""
23+
model_id_lower = model_id.lower()
24+
if exclude_terms and any(term in model_id_lower for term in exclude_terms):
25+
return False
26+
if include_terms:
27+
return any(term in model_id_lower for term in include_terms)
28+
return True
29+
30+
1431
def _has_user_overrides(model: Model) -> bool:
1532
"""
1633
Check if a model has any user-applied configuration overrides.
@@ -71,16 +88,19 @@ async def sync_provider(session, config, provider, push_to_litellm: bool = True)
7188
logger.info("Found %d models from %s", len(source_models.models), provider.name)
7289

7390
# Apply model filter if configured
74-
if provider.model_filter:
91+
include_terms = _parse_filter_terms(provider.model_filter)
92+
exclude_terms = _parse_filter_terms(provider.model_filter_exclude)
93+
if include_terms or exclude_terms:
7594
original_count = len(source_models.models)
7695
filtered_models = [
7796
m for m in source_models.models
78-
if provider.model_filter.lower() in m.id.lower()
97+
if _matches_include_exclude(m.id, include_terms, exclude_terms)
7998
]
8099
source_models.models = filtered_models
81100
logger.info(
82-
"Applied filter '%s': %d models matched (filtered out %d)",
83-
provider.model_filter,
101+
"Applied filters include='%s' exclude='%s': %d models matched (filtered out %d)",
102+
provider.model_filter or "",
103+
provider.model_filter_exclude or "",
84104
len(filtered_models),
85105
original_count - len(filtered_models)
86106
)

frontend/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Frontend API and UI service."""
22

3-
__version__ = "0.6.20"
3+
__version__ = "0.6.21"

frontend/api.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from shared.database import create_engine, init_session_maker, get_session, ensure_minimum_schema
1818
from shared.crud import get_all_providers, get_config, get_provider_by_id
19-
from frontend.routes import providers, models, admin, compat, litellm
19+
from frontend.routes import providers, models, admin, compat, litellm, routing_groups
2020
from backend import provider_sync
2121
from sqlalchemy import select, func, case
2222
from shared.db_models import Model, Provider
@@ -96,6 +96,7 @@ def _human_source_type(source_type: str) -> str:
9696
app.include_router(models.router, prefix="/api/models", tags=["models"])
9797
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
9898
app.include_router(compat.router, prefix="/api/compat", tags=["compat"])
99+
app.include_router(routing_groups.router, prefix="/api/routing-groups", tags=["routing-groups"])
99100
app.include_router(litellm.router, prefix="/litellm", tags=["litellm"])
100101

101102
# HTML Routes
@@ -359,6 +360,13 @@ async def settings_page(request: Request, session = Depends(get_session)):
359360
"config": config_dict
360361
})
361362

363+
@app.get("/routing", response_class=HTMLResponse)
364+
async def routing_groups_page(request: Request):
365+
"""Routing group configuration page."""
366+
return templates.TemplateResponse("routing_groups.html", {
367+
"request": request
368+
})
369+
362370
@app.post("/sync")
363371
async def manual_sync(request: Request, session = Depends(get_session)):
364372
"""
@@ -451,6 +459,7 @@ async def create_provider_legacy(
451459
prefix: str | None = Form(None),
452460
default_ollama_mode: str | None = Form(None),
453461
model_filter: str | None = Form(None),
462+
model_filter_exclude: str | None = Form(None),
454463
sync_interval_seconds: int | None = Form(None),
455464
session: AsyncSession = Depends(get_session)
456465
):
@@ -465,6 +474,7 @@ async def create_provider_legacy(
465474
prefix=prefix,
466475
default_ollama_mode=default_ollama_mode,
467476
model_filter=model_filter,
477+
model_filter_exclude=model_filter_exclude,
468478
sync_interval_seconds=sync_interval_seconds
469479
)
470480
from fastapi.responses import RedirectResponse

frontend/routes/providers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ async def list_providers(session: AsyncSession = Depends(get_session)):
7878
"default_ollama_mode": p.default_ollama_mode,
7979
"auto_detect_fim": p.auto_detect_fim,
8080
"model_filter": p.model_filter,
81+
"model_filter_exclude": p.model_filter_exclude,
8182
"tags": p.tags_list,
8283
"access_groups": p.access_groups_list,
8384
"sync_enabled": p.sync_enabled,
@@ -282,6 +283,7 @@ async def get_provider(provider_id: int, session: AsyncSession = Depends(get_ses
282283
"default_ollama_mode": provider.default_ollama_mode,
283284
"auto_detect_fim": provider.auto_detect_fim,
284285
"model_filter": provider.model_filter,
286+
"model_filter_exclude": provider.model_filter_exclude,
285287
"tags": provider.tags_list,
286288
"access_groups": provider.access_groups_list,
287289
"sync_enabled": provider.sync_enabled,
@@ -301,6 +303,7 @@ async def add_provider(
301303
prefix: str | None = Form(None),
302304
default_ollama_mode: str | None = Form(None),
303305
model_filter: str | None = Form(None),
306+
model_filter_exclude: str | None = Form(None),
304307
tags: str | None = Form(None),
305308
access_groups: str | None = Form(None),
306309
sync_enabled: bool | None = Form(True),
@@ -327,6 +330,7 @@ async def add_provider(
327330
prefix=_normalize_optional_str(prefix),
328331
default_ollama_mode=_normalize_optional_str(default_ollama_mode),
329332
model_filter=_normalize_optional_str(model_filter),
333+
model_filter_exclude=_normalize_optional_str(model_filter_exclude),
330334
tags=_parse_csv_list(tags),
331335
access_groups=_parse_csv_list(access_groups),
332336
sync_enabled=sync_enabled_val,
@@ -393,6 +397,7 @@ async def update_provider_endpoint(
393397
prefix: str | None = Form(None),
394398
default_ollama_mode: str | None = Form(None),
395399
model_filter: str | None = Form(None),
400+
model_filter_exclude: str | None = Form(None),
396401
tags: str | None = Form(None),
397402
access_groups: str | None = Form(None),
398403
sync_enabled: bool | None = Form(None),
@@ -418,6 +423,7 @@ async def update_provider_endpoint(
418423
prefix=_normalize_optional_str(prefix),
419424
default_ollama_mode=_normalize_optional_str(default_ollama_mode),
420425
model_filter=_normalize_optional_str(model_filter),
426+
model_filter_exclude=_normalize_optional_str(model_filter_exclude),
421427
tags=_parse_csv_list(tags),
422428
access_groups=_parse_csv_list(access_groups),
423429
sync_enabled=_parse_bool(sync_enabled),

0 commit comments

Comments
 (0)