model-mesh-ai 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- model_mesh_ai-1.0.0/PKG-INFO +8 -0
- model_mesh_ai-1.0.0/README.md +40 -0
- model_mesh_ai-1.0.0/model_mesh/__init__.py +20 -0
- model_mesh_ai-1.0.0/model_mesh/balancer.py +189 -0
- model_mesh_ai-1.0.0/model_mesh/client.py +694 -0
- model_mesh_ai-1.0.0/model_mesh/rate_limiter.py +115 -0
- model_mesh_ai-1.0.0/model_mesh/standardizer.py +241 -0
- model_mesh_ai-1.0.0/model_mesh_ai.egg-info/PKG-INFO +8 -0
- model_mesh_ai-1.0.0/model_mesh_ai.egg-info/SOURCES.txt +11 -0
- model_mesh_ai-1.0.0/model_mesh_ai.egg-info/dependency_links.txt +1 -0
- model_mesh_ai-1.0.0/model_mesh_ai.egg-info/top_level.txt +1 -0
- model_mesh_ai-1.0.0/setup.cfg +4 -0
- model_mesh_ai-1.0.0/setup.py +14 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: model-mesh-ai
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Model Mesh: Unified AI Gateway SDK with Standardized Requests, Load Balancing, and Rate Limiting
|
|
5
|
+
Author: Model Mesh Team
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.8
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Model Mesh - Python SDK
|
|
2
|
+
|
|
3
|
+
A unified, high-performance package for interacting with Large Language Models with **Standardized Request/Response**, **Load Balancing**, and **Rate Limiting**.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd sdk/python
|
|
9
|
+
pip install -e .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import model_mesh
|
|
16
|
+
|
|
17
|
+
# 1. Initialize Model Mesh
|
|
18
|
+
mesh = model_mesh.Client(
|
|
19
|
+
base_url="http://localhost:8000/v1",
|
|
20
|
+
api_key="sk-modelmesh-client-key",
|
|
21
|
+
rate_limit="20/min",
|
|
22
|
+
load_balancing="round_robin"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# 2. Method 1: Standardized Chat (Unified Request & Response)
|
|
26
|
+
response = mesh.chat(
|
|
27
|
+
model="gpt-4o",
|
|
28
|
+
messages=[{"role": "user", "content": "Explain Model Mesh!"}]
|
|
29
|
+
)
|
|
30
|
+
print("Assistant:", response.content)
|
|
31
|
+
print("Tokens Used:", response.usage)
|
|
32
|
+
|
|
33
|
+
# 3. Method 2: Inspect Load Balancing
|
|
34
|
+
route = mesh.balance(model="claude-3-5-sonnet")
|
|
35
|
+
print("Routed to channel:", route["selected_channel"])
|
|
36
|
+
|
|
37
|
+
# 4. Method 3: Check Rate Limiting
|
|
38
|
+
status = mesh.check_rate_limit(client_id="user_123")
|
|
39
|
+
print("Remaining calls:", status["remaining_requests"])
|
|
40
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model Mesh Python SDK
|
|
3
|
+
A unified client and library for Model Mesh - The High-Performance AI Gateway.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .standardizer import Standardizer, ChatResponse
|
|
7
|
+
from .balancer import LoadBalancer
|
|
8
|
+
from .rate_limiter import RateLimiter, RateLimitExceededError
|
|
9
|
+
from .client import ModelMesh, Client
|
|
10
|
+
|
|
11
|
+
__version__ = "1.0.0"
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ModelMesh",
|
|
14
|
+
"Client",
|
|
15
|
+
"LoadBalancer",
|
|
16
|
+
"RateLimiter",
|
|
17
|
+
"Standardizer",
|
|
18
|
+
"ChatResponse",
|
|
19
|
+
"RateLimitExceededError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model Mesh Load Balancer & Fault-Tolerance Engine
|
|
3
|
+
Provides round-robin, weighted, priority failover, and circuit-breaker cooling across AI channels.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import random
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple, Set
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Channel:
|
|
14
|
+
name: str
|
|
15
|
+
base_url: str
|
|
16
|
+
api_keys: List[str]
|
|
17
|
+
models: List[str] = field(default_factory=lambda: ["*"])
|
|
18
|
+
weight: int = 1
|
|
19
|
+
priority: int = 1 # 1 = primary, 2 = secondary/fallback, etc.
|
|
20
|
+
key_index: int = 0
|
|
21
|
+
failure_count: int = 0
|
|
22
|
+
success_count: int = 0
|
|
23
|
+
cooldown_until: float = 0.0
|
|
24
|
+
simulated_error: Optional[str] = None # e.g., "429: Too Many Requests" or "500: Internal Server Error"
|
|
25
|
+
|
|
26
|
+
def is_healthy(self) -> bool:
|
|
27
|
+
"""Returns True if the channel is not currently cooled down and not manually faulted."""
|
|
28
|
+
return time.time() >= self.cooldown_until and self.simulated_error is None
|
|
29
|
+
|
|
30
|
+
def get_next_key(self) -> str:
|
|
31
|
+
"""Round-robin through the keys of this channel."""
|
|
32
|
+
if not self.api_keys:
|
|
33
|
+
return ""
|
|
34
|
+
key = self.api_keys[self.key_index % len(self.api_keys)]
|
|
35
|
+
self.key_index = (self.key_index + 1) % len(self.api_keys)
|
|
36
|
+
return key
|
|
37
|
+
|
|
38
|
+
def trigger_cooldown(self, seconds: float = 30.0, reason: str = "Failure"):
|
|
39
|
+
"""Mark channel as cooled down after an upstream failure."""
|
|
40
|
+
self.failure_count += 1
|
|
41
|
+
self.cooldown_until = time.time() + seconds
|
|
42
|
+
|
|
43
|
+
def mark_success(self):
|
|
44
|
+
"""Record success and reset cooldown."""
|
|
45
|
+
self.success_count += 1
|
|
46
|
+
self.cooldown_until = 0.0
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
49
|
+
now = time.time()
|
|
50
|
+
return {
|
|
51
|
+
"name": self.name,
|
|
52
|
+
"base_url": self.base_url,
|
|
53
|
+
"weight": self.weight,
|
|
54
|
+
"priority": self.priority,
|
|
55
|
+
"key_count": len(self.api_keys),
|
|
56
|
+
"healthy": self.is_healthy(),
|
|
57
|
+
"failure_count": self.failure_count,
|
|
58
|
+
"success_count": self.success_count,
|
|
59
|
+
"cooldown_remaining_sec": max(0.0, round(self.cooldown_until - now, 1)),
|
|
60
|
+
"simulated_error": self.simulated_error,
|
|
61
|
+
"supported_models": self.models,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LoadBalancer:
|
|
66
|
+
"""
|
|
67
|
+
Intelligent router with Round-Robin, Weighted balancing, Priority Failover,
|
|
68
|
+
and Circuit-Breaker Cooldown recovery.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, algorithm: str = "round_robin"):
|
|
72
|
+
self.algorithm = algorithm.lower()
|
|
73
|
+
self.channels: Dict[str, Channel] = {}
|
|
74
|
+
self._round_robin_index: int = 0
|
|
75
|
+
|
|
76
|
+
def add_channel(
|
|
77
|
+
self,
|
|
78
|
+
name: str,
|
|
79
|
+
base_url: str,
|
|
80
|
+
api_keys: Optional[List[str]] = None,
|
|
81
|
+
models: Optional[List[str]] = None,
|
|
82
|
+
weight: int = 1,
|
|
83
|
+
priority: int = 1
|
|
84
|
+
) -> Channel:
|
|
85
|
+
"""Register a provider channel with custom keys, models, weight, and priority."""
|
|
86
|
+
if api_keys is None:
|
|
87
|
+
api_keys = []
|
|
88
|
+
elif isinstance(api_keys, str):
|
|
89
|
+
api_keys = [api_keys]
|
|
90
|
+
|
|
91
|
+
channel = Channel(
|
|
92
|
+
name=name,
|
|
93
|
+
base_url=base_url,
|
|
94
|
+
api_keys=api_keys,
|
|
95
|
+
models=models or ["*"],
|
|
96
|
+
weight=max(1, weight),
|
|
97
|
+
priority=priority
|
|
98
|
+
)
|
|
99
|
+
self.channels[name] = channel
|
|
100
|
+
return channel
|
|
101
|
+
|
|
102
|
+
def remove_channel(self, name: str) -> bool:
|
|
103
|
+
"""Remove a channel from the balancer."""
|
|
104
|
+
if name in self.channels:
|
|
105
|
+
del self.channels[name]
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def simulate_fault(self, name: str, error_message: str = "HTTP 429: Rate limit reached on upstream"):
|
|
110
|
+
"""Inject a simulated failure into a channel to test fallback."""
|
|
111
|
+
if name in self.channels:
|
|
112
|
+
self.channels[name].simulated_error = error_message
|
|
113
|
+
self.channels[name].trigger_cooldown(45.0)
|
|
114
|
+
|
|
115
|
+
def clear_fault(self, name: str):
|
|
116
|
+
"""Restore channel to healthy state."""
|
|
117
|
+
if name in self.channels:
|
|
118
|
+
self.channels[name].simulated_error = None
|
|
119
|
+
self.channels[name].cooldown_until = 0.0
|
|
120
|
+
|
|
121
|
+
def _get_eligible_channels(
|
|
122
|
+
self,
|
|
123
|
+
model: Optional[str] = None,
|
|
124
|
+
excluded: Optional[Set[str]] = None,
|
|
125
|
+
include_faulted: bool = False
|
|
126
|
+
) -> List[Channel]:
|
|
127
|
+
"""Find channels that support the requested model and are not excluded."""
|
|
128
|
+
excluded = excluded or set()
|
|
129
|
+
eligible = []
|
|
130
|
+
for ch in self.channels.values():
|
|
131
|
+
if ch.name in excluded:
|
|
132
|
+
continue
|
|
133
|
+
if not include_faulted and not ch.is_healthy():
|
|
134
|
+
continue
|
|
135
|
+
if model is None or "*" in ch.models or model in ch.models:
|
|
136
|
+
eligible.append(ch)
|
|
137
|
+
return eligible
|
|
138
|
+
|
|
139
|
+
def select_channel(
|
|
140
|
+
self,
|
|
141
|
+
model: Optional[str] = None,
|
|
142
|
+
excluded: Optional[Set[str]] = None,
|
|
143
|
+
include_faulted: bool = False
|
|
144
|
+
) -> Tuple[Channel, str]:
|
|
145
|
+
"""
|
|
146
|
+
Select best channel and key. If channels are tiered by priority,
|
|
147
|
+
highest priority (lowest number) healthy channels are picked first.
|
|
148
|
+
"""
|
|
149
|
+
eligible = self._get_eligible_channels(model, excluded, include_faulted=include_faulted)
|
|
150
|
+
|
|
151
|
+
# Fallback 1: If no healthy channels with priority, try any non-excluded channel
|
|
152
|
+
if not eligible and self.channels:
|
|
153
|
+
non_excluded = [c for c in self.channels.values() if c.name not in (excluded or set())]
|
|
154
|
+
if non_excluded:
|
|
155
|
+
# pick the one with earliest cooldown expiry
|
|
156
|
+
eligible = sorted(non_excluded, key=lambda c: c.cooldown_until)[:1]
|
|
157
|
+
|
|
158
|
+
if not eligible:
|
|
159
|
+
raise RuntimeError(f"No available channels found in Model Mesh for model '{model}'.")
|
|
160
|
+
|
|
161
|
+
# Priority grouping: pick channels in the lowest available priority level
|
|
162
|
+
min_priority = min(c.priority for c in eligible)
|
|
163
|
+
tier = [c for c in eligible if c.priority == min_priority]
|
|
164
|
+
|
|
165
|
+
# 1. Weighted Selection within Tier
|
|
166
|
+
if self.algorithm == "weighted":
|
|
167
|
+
weights = [c.weight for c in tier]
|
|
168
|
+
chosen_channel = random.choices(tier, weights=weights, k=1)[0]
|
|
169
|
+
# 2. Round Robin Selection
|
|
170
|
+
else:
|
|
171
|
+
chosen_channel = tier[self._round_robin_index % len(tier)]
|
|
172
|
+
self._round_robin_index = (self._round_robin_index + 1) % len(tier)
|
|
173
|
+
|
|
174
|
+
selected_key = chosen_channel.get_next_key()
|
|
175
|
+
return chosen_channel, selected_key
|
|
176
|
+
|
|
177
|
+
def report_failure(self, channel_name: str, cooldown_seconds: float = 30.0):
|
|
178
|
+
"""Record upstream error and cooldown."""
|
|
179
|
+
if channel_name in self.channels:
|
|
180
|
+
self.channels[channel_name].trigger_cooldown(cooldown_seconds)
|
|
181
|
+
|
|
182
|
+
def report_success(self, channel_name: str):
|
|
183
|
+
"""Record successful response."""
|
|
184
|
+
if channel_name in self.channels:
|
|
185
|
+
self.channels[channel_name].mark_success()
|
|
186
|
+
|
|
187
|
+
def get_status(self) -> List[Dict[str, Any]]:
|
|
188
|
+
"""Return snapshot of all channels."""
|
|
189
|
+
return [c.to_dict() for c in self.channels.values()]
|