This repository was archived by the owner on Jun 5, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 91
Expand file tree
/
Copy pathdetector.py
More file actions
245 lines (194 loc) · 7.37 KB
/
detector.py
File metadata and controls
245 lines (194 loc) · 7.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
from abc import ABC, abstractmethod
from functools import wraps
from typing import List, Optional
import regex as re
import structlog
from fastapi import Request
from codegate.clients.clients import ClientType
logger = structlog.get_logger("codegate")
class HeaderDetector:
"""
Base utility class for header-based detection
"""
def __init__(self, header_name: str, header_value: Optional[str] = None):
self.header_name = header_name
self.header_value = header_value
def detect(self, request: Request) -> bool:
logger.debug(
"checking header detection",
header_name=self.header_name,
header_value=self.header_value,
request_headers=dict(request.headers),
)
# Check if the header is present, if not we didn't detect the client
if self.header_name not in request.headers:
return False
# now we know that the header is present, if we don't care about the value
# we detected the client
if self.header_value is None:
return True
# finally, if we care about the value, we need to check if it matches
return request.headers[self.header_name] == self.header_value
class UserAgentDetector(HeaderDetector):
"""
A variant of the HeaderDetector that specifically looks for a user-agent pattern
"""
def __init__(self, user_agent_pattern: str):
super().__init__("user-agent")
self.pattern = re.compile(user_agent_pattern, re.IGNORECASE)
def detect(self, request: Request) -> bool:
user_agent = request.headers.get(self.header_name)
if not user_agent:
return False
return bool(self.pattern.search(user_agent))
class ContentDetector:
"""
Detector for message content patterns
"""
def __init__(self, pattern: str):
self.pattern = pattern
async def detect(self, request: Request) -> bool:
try:
data = await request.json()
for message in data.get("messages", []):
message_content = str(message.get("content", ""))
if self.pattern in message_content:
return True
# This is clearly a hack and won't be needed when we get rid of the normalizers and will
# be able to access the system message directly from the on-wire format
system_content = str(data.get("system", ""))
if self.pattern in system_content:
return True
return False
except Exception as e:
logger.error(f"Error in content detection: {str(e)}")
return False
class BaseClientDetector(ABC):
"""
Base class for all client detectors using composition of detection methods
"""
def __init__(self):
self.header_detector: Optional[HeaderDetector] = None
self.user_agent_detector: Optional[UserAgentDetector] = None
self.content_detector: Optional[ContentDetector] = None
@property
@abstractmethod
def client_name(self) -> ClientType:
"""
Returns the name of the client
"""
pass
async def detect(self, request: Request) -> bool:
"""
Tries each configured detection method in sequence
"""
# Try user agent first if configured
if self.user_agent_detector and self.user_agent_detector.detect(request):
return True
# Then try header if configured
if self.header_detector and self.header_detector.detect(request):
return True
# Finally try content if configured
if self.content_detector:
return await self.content_detector.detect(request)
return False
class ClineDetector(BaseClientDetector):
"""
Detector for Cline client based on message content
"""
def __init__(self):
super().__init__()
self.content_detector = ContentDetector("Cline")
@property
def client_name(self) -> ClientType:
return ClientType.CLINE
class KoduDetector(BaseClientDetector):
"""
Detector for Kodu client based on message content
"""
def __init__(self):
super().__init__()
self.user_agent_detector = UserAgentDetector("Kodu")
self.content_detector = ContentDetector("Kodu")
@property
def client_name(self) -> ClientType:
return ClientType.KODU
class OpenInterpreter(BaseClientDetector):
"""
Detector for Kodu client based on message content
"""
def __init__(self):
super().__init__()
self.content_detector = ContentDetector("Open Interpreter")
@property
def client_name(self) -> ClientType:
return ClientType.OPEN_INTERPRETER
class ContinueDetector(BaseClientDetector):
"""
Detector for Continue client based on message content
"""
def __init__(self):
super().__init__()
# This is a hack that really only detects Continue with DeepSeek
# we should get a header or user agent for this (upstream PR pending)
self.content_detector = ContentDetector(
"You are an AI programming assistant, utilizing the DeepSeek Coder model"
)
@property
def client_name(self) -> ClientType:
return ClientType.CONTINUE
class CopilotDetector(BaseClientDetector):
"""
Detector for Copilot client based on user agent
"""
def __init__(self):
super().__init__()
self.header_detector = HeaderDetector("user-agent", "Copilot")
self.user_agent_detector = UserAgentDetector("Copilot")
@property
def client_name(self) -> ClientType:
return ClientType.COPILOT
class DetectClient:
"""
Decorator class for detecting clients from request system messages
Usage:
@app.post("/v1/chat/completions")
@DetectClient()
async def chat_completions(request: Request):
client = request.state.detected_client
"""
def __init__(self):
self.detectors: List[BaseClientDetector] = [
ClineDetector(),
KoduDetector(),
OpenInterpreter(),
CopilotDetector(),
ContinueDetector(),
]
def __call__(self, func):
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
try:
client = await self.detect(request)
request.state.detected_client = client
except Exception as e:
logger.error(f"Error in client detection: {str(e)}")
request.state.detected_client = ClientType.GENERIC
return await func(request, *args, **kwargs)
return wrapper
async def detect(self, request: Request) -> ClientType:
"""
Detects the client from the request by trying each detector in sequence.
Returns the name of the first detected client, or GENERIC if no specific client is detected.
"""
for detector in self.detectors:
try:
if await detector.detect(request):
client_name = detector.client_name
logger.info(f"{client_name} client detected")
return client_name
except Exception as e:
logger.error(f"Error in {detector.client_name} detection: {str(e)}")
continue
logger.info("No particilar client detected, using generic client")
return ClientType.GENERIC