Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit d8b7cbf

Browse files
Update the muxing rules to v3 (#1112)
* Update the muxing rules to v3 Closes: #1060 Right now the muxing rules are designed to catch globally FIM or Chat requests. This PR extends its functionality to be able to match per file and request, i.e. this PR enables - Chat request of main.py -> model 1 - FIM request of main.py -> model 2 - Any type of v1.py -> model 3 * updated matcher types to preserve old types
1 parent d5a5808 commit d8b7cbf

File tree

5 files changed

+257
-89
lines changed

5 files changed

+257
-89
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""update matcher types
2+
3+
Revision ID: 5e5cd2288147
4+
Revises: 0c3539f66339
5+
Create Date: 2025-02-19 14:52:39.126196+00:00
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str = "5e5cd2288147"
15+
down_revision: Union[str, None] = "0c3539f66339"
16+
branch_labels: Union[str, Sequence[str], None] = None
17+
depends_on: Union[str, Sequence[str], None] = None
18+
19+
20+
def upgrade() -> None:
21+
# Begin transaction
22+
op.execute("BEGIN TRANSACTION;")
23+
24+
# Update the matcher types. We need to do this every time we change the matcher types.
25+
# in /muxing/models.py
26+
op.execute(
27+
"""
28+
UPDATE muxes
29+
SET matcher_type = 'fim_filename', matcher_blob = ''
30+
WHERE matcher_type = 'request_type_match' AND matcher_blob = 'fim';
31+
"""
32+
)
33+
op.execute(
34+
"""
35+
UPDATE muxes
36+
SET matcher_type = 'chat_filename', matcher_blob = ''
37+
WHERE matcher_type = 'request_type_match' AND matcher_blob = 'chat';
38+
"""
39+
)
40+
41+
# Finish transaction
42+
op.execute("COMMIT;")
43+
44+
45+
def downgrade() -> None:
46+
# Begin transaction
47+
op.execute("BEGIN TRANSACTION;")
48+
49+
op.execute(
50+
"""
51+
UPDATE muxes
52+
SET matcher_blob = 'fim', matcher_type = 'request_type_match'
53+
WHERE matcher_type = 'fim';
54+
"""
55+
)
56+
op.execute(
57+
"""
58+
UPDATE muxes
59+
SET matcher_blob = 'chat', matcher_type = 'request_type_match'
60+
WHERE matcher_type = 'chat';
61+
"""
62+
)
63+
64+
# Finish transaction
65+
op.execute("COMMIT;")

src/codegate/muxing/models.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,37 @@
11
from enum import Enum
2-
from typing import Optional
2+
from typing import Optional, Self
33

44
import pydantic
55

66
from codegate.clients.clients import ClientType
7+
from codegate.db.models import MuxRule as DBMuxRule
78

89

910
class MuxMatcherType(str, Enum):
1011
"""
1112
Represents the different types of matchers we support.
13+
14+
The 3 rules present match filenames and request types. They're used in conjunction with the
15+
matcher field in the MuxRule model.
16+
E.g.
17+
- catch_all-> Always match
18+
- filename_match and match: requests.py -> Match the request if the filename is requests.py
19+
- fim_filename and match: main.py -> Match the request if the request type is fim
20+
and the filename is main.py
21+
22+
NOTE: Removing or updating fields from this enum will require a migration.
23+
Adding new fields is safe.
1224
"""
1325

1426
# Always match this prompt
1527
catch_all = "catch_all"
1628
# Match based on the filename. It will match if there is a filename
1729
# in the request that matches the matcher either extension or full name (*.py or main.py)
1830
filename_match = "filename_match"
19-
# Match based on the request type. It will match if the request type
20-
# matches the matcher (e.g. FIM or chat)
21-
request_type_match = "request_type_match"
31+
# Match based on fim request type. It will match if the request type is fim
32+
fim_filename = "fim_filename"
33+
# Match based on chat request type. It will match if the request type is chat
34+
chat_filename = "chat_filename"
2235

2336

2437
class MuxRule(pydantic.BaseModel):
@@ -36,6 +49,18 @@ class MuxRule(pydantic.BaseModel):
3649
# this depends on the matcher type.
3750
matcher: Optional[str] = None
3851

52+
@classmethod
53+
def from_db_mux_rule(cls, db_mux_rule: DBMuxRule) -> Self:
54+
"""
55+
Convert a DBMuxRule to a MuxRule.
56+
"""
57+
return MuxRule(
58+
provider_id=db_mux_rule.id,
59+
model=db_mux_rule.provider_model_name,
60+
matcher_type=db_mux_rule.matcher_type,
61+
matcher=db_mux_rule.matcher_blob,
62+
)
63+
3964

4065
class ThingToMatchMux(pydantic.BaseModel):
4166
"""

src/codegate/muxing/router.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ async def _get_model_route(
5050
# Try to get a model route for the active workspace
5151
model_route = await mux_registry.get_match_for_active_workspace(thing_to_match)
5252
return model_route
53+
except rulematcher.MuxMatchingError as e:
54+
logger.exception(f"Error matching rule and getting model route: {e}")
55+
raise HTTPException(detail=str(e), status_code=404)
5356
except Exception as e:
54-
logger.error(f"Error getting active workspace muxes: {e}")
57+
logger.exception(f"Error getting active workspace muxes: {e}")
5558
raise HTTPException(detail=str(e), status_code=404)
5659

5760
def _setup_routes(self):

src/codegate/muxing/rulematcher.py

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
_singleton_lock = Lock()
1919

2020

21+
class MuxMatchingError(Exception):
22+
"""An exception for muxing matching errors."""
23+
24+
pass
25+
26+
2127
async def get_muxing_rules_registry():
2228
"""Returns a singleton instance of the muxing rules registry."""
2329

@@ -48,9 +54,9 @@ def __init__(
4854
class MuxingRuleMatcher(ABC):
4955
"""Base class for matching muxing rules."""
5056

51-
def __init__(self, route: ModelRoute, matcher_blob: str):
57+
def __init__(self, route: ModelRoute, mux_rule: mux_models.MuxRule):
5258
self._route = route
53-
self._matcher_blob = matcher_blob
59+
self._mux_rule = mux_rule
5460

5561
@abstractmethod
5662
def match(self, thing_to_match: mux_models.ThingToMatchMux) -> bool:
@@ -67,18 +73,20 @@ class MuxingMatcherFactory:
6773
"""Factory for creating muxing matchers."""
6874

6975
@staticmethod
70-
def create(mux_rule: db_models.MuxRule, route: ModelRoute) -> MuxingRuleMatcher:
76+
def create(db_mux_rule: db_models.MuxRule, route: ModelRoute) -> MuxingRuleMatcher:
7177
"""Create a muxing matcher for the given endpoint and model."""
7278

7379
factory: Dict[mux_models.MuxMatcherType, MuxingRuleMatcher] = {
7480
mux_models.MuxMatcherType.catch_all: CatchAllMuxingRuleMatcher,
7581
mux_models.MuxMatcherType.filename_match: FileMuxingRuleMatcher,
76-
mux_models.MuxMatcherType.request_type_match: RequestTypeMuxingRuleMatcher,
82+
mux_models.MuxMatcherType.fim_filename: RequestTypeAndFileMuxingRuleMatcher,
83+
mux_models.MuxMatcherType.chat_filename: RequestTypeAndFileMuxingRuleMatcher,
7784
}
7885

7986
try:
8087
# Initialize the MuxingRuleMatcher
81-
return factory[mux_rule.matcher_type](route, mux_rule.matcher_blob)
88+
mux_rule = mux_models.MuxRule.from_db_mux_rule(db_mux_rule)
89+
return factory[mux_rule.matcher_type](route, mux_rule)
8290
except KeyError:
8391
raise ValueError(f"Unknown matcher type: {mux_rule.matcher_type}")
8492

@@ -103,47 +111,63 @@ def _extract_request_filenames(self, detected_client: ClientType, data: dict) ->
103111
return body_extractor.extract_unique_filenames(data)
104112
except BodyCodeSnippetExtractorError as e:
105113
logger.error(f"Error extracting filenames from request: {e}")
106-
return set()
114+
raise MuxMatchingError("Error extracting filenames from request")
115+
116+
def _is_matcher_in_filenames(self, detected_client: ClientType, data: dict) -> bool:
117+
"""
118+
Check if the matcher is in the request filenames.
119+
"""
120+
# Empty matcher_blob means we match everything
121+
if not self._mux_rule.matcher:
122+
return True
123+
filenames_to_match = self._extract_request_filenames(detected_client, data)
124+
# _mux_rule.matcher can be a filename or a file extension. We match if any of the filenames
125+
# match the rule.
126+
is_filename_match = any(
127+
self._mux_rule.matcher == filename or filename.endswith(self._mux_rule.matcher)
128+
for filename in filenames_to_match
129+
)
130+
return is_filename_match
107131

108132
def match(self, thing_to_match: mux_models.ThingToMatchMux) -> bool:
109133
"""
110-
Retun True if there is a filename in the request that matches the matcher_blob.
111-
The matcher_blob is either an extension (e.g. .py) or a filename (e.g. main.py).
134+
Return True if the matcher is in one of the request filenames.
112135
"""
113-
# If there is no matcher_blob, we don't match
114-
if not self._matcher_blob:
115-
return False
116-
filenames_to_match = self._extract_request_filenames(
136+
is_rule_matched = self._is_matcher_in_filenames(
117137
thing_to_match.client_type, thing_to_match.body
118138
)
119-
is_filename_match = any(self._matcher_blob in filename for filename in filenames_to_match)
120-
if is_filename_match:
121-
logger.info(
122-
"Filename rule matched", filenames=filenames_to_match, matcher=self._matcher_blob
123-
)
124-
return is_filename_match
139+
if is_rule_matched:
140+
logger.info("Filename rule matched", matcher=self._mux_rule.matcher)
141+
return is_rule_matched
125142

126143

127-
class RequestTypeMuxingRuleMatcher(MuxingRuleMatcher):
128-
"""A catch all muxing rule matcher."""
144+
class RequestTypeAndFileMuxingRuleMatcher(FileMuxingRuleMatcher):
145+
"""A request type and file muxing rule matcher."""
146+
147+
def _is_request_type_match(self, is_fim_request: bool) -> bool:
148+
"""
149+
Check if the request type matches the MuxMatcherType.
150+
"""
151+
incoming_request_type = "fim_filename" if is_fim_request else "chat_filename"
152+
if incoming_request_type == self._mux_rule.matcher_type:
153+
return True
154+
return False
129155

130156
def match(self, thing_to_match: mux_models.ThingToMatchMux) -> bool:
131157
"""
132-
Return True if the request type matches the matcher_blob.
133-
The matcher_blob is either "fim" or "chat".
158+
Return True if the matcher is in one of the request filenames and
159+
if the request type matches the MuxMatcherType.
134160
"""
135-
# If there is no matcher_blob, we don't match
136-
if not self._matcher_blob:
137-
return False
138-
incoming_request_type = "fim" if thing_to_match.is_fim_request else "chat"
139-
is_request_type_match = self._matcher_blob == incoming_request_type
140-
if is_request_type_match:
161+
is_rule_matched = self._is_matcher_in_filenames(
162+
thing_to_match.client_type, thing_to_match.body
163+
) and self._is_request_type_match(thing_to_match.is_fim_request)
164+
if is_rule_matched:
141165
logger.info(
142-
"Request type rule matched",
143-
matcher=self._matcher_blob,
144-
request_type=incoming_request_type,
166+
"Request type and rule matched",
167+
matcher=self._mux_rule.matcher,
168+
is_fim_request=thing_to_match.is_fim_request,
145169
)
146-
return is_request_type_match
170+
return is_rule_matched
147171

148172

149173
class MuxingRulesinWorkspaces:

0 commit comments

Comments
 (0)