keycycle 0.2.2__tar.gz → 0.2.4__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.
- {keycycle-0.2.2 → keycycle-0.2.4}/PKG-INFO +1 -1
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/adapters/openai_adapter.py +1 -1
- keycycle-0.2.4/keycycle/config/models/moonshot.yaml +59 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/models.py +5 -2
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/key_rotation/rotating_mixin.py +18 -10
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/utils.py +2 -1
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle.egg-info/PKG-INFO +1 -1
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle.egg-info/SOURCES.txt +1 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/pyproject.toml +2 -1
- {keycycle-0.2.2 → keycycle-0.2.4}/MANIFEST.in +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/__init__.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/adapters/__init__.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/adapters/generic_adapter.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/__init__.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/constants.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/dataclasses.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/enums.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/loader.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/log_config.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/models/cerebras.yaml +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/models/cohere.yaml +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/models/gemini.yaml +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/models/groq.yaml +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/models/openrouter.yaml +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/config/models/openrouter_models.yaml +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/core/__init__.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/core/backoff.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/core/exceptions.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/core/utils.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/key_rotation/__init__.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/key_rotation/rotation_manager.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/legacy_multi_provider_wrapper.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/multi_client_wrapper.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/py.typed +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/usage/__init__.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/usage/db_logic.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle/usage/usage_logger.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle.egg-info/dependency_links.txt +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle.egg-info/requires.txt +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/keycycle.egg-info/top_level.txt +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/setup.cfg +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_api_key_pinning.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_api_key_retrieval.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_cerebras_direct.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_custom_key_limits.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_multi_client_wrapper.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_rate_limit_logic.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_specific_key_retrieval.py +0 -0
- {keycycle-0.2.2 → keycycle-0.2.4}/tests/test_streams_recording.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Moonshot API rate limits (dummy high limits)
|
|
2
|
+
# Base URL: https://api.moonshot.ai/v1
|
|
3
|
+
# Agno class: MoonShot (similar to OpenRouter special case)
|
|
4
|
+
|
|
5
|
+
default:
|
|
6
|
+
requests_per_minute: 10000
|
|
7
|
+
requests_per_hour: 100000
|
|
8
|
+
requests_per_day: 1000000
|
|
9
|
+
tokens_per_minute: 10000000
|
|
10
|
+
tokens_per_hour: 100000000
|
|
11
|
+
tokens_per_day: 1000000000
|
|
12
|
+
|
|
13
|
+
kimi-k2.5:
|
|
14
|
+
requests_per_minute: 10000
|
|
15
|
+
requests_per_hour: 100000
|
|
16
|
+
requests_per_day: 1000000
|
|
17
|
+
tokens_per_minute: 10000000
|
|
18
|
+
tokens_per_hour: 100000000
|
|
19
|
+
tokens_per_day: 1000000000
|
|
20
|
+
|
|
21
|
+
kimi-k2-0905-preview:
|
|
22
|
+
requests_per_minute: 10000
|
|
23
|
+
requests_per_hour: 100000
|
|
24
|
+
requests_per_day: 1000000
|
|
25
|
+
tokens_per_minute: 10000000
|
|
26
|
+
tokens_per_hour: 100000000
|
|
27
|
+
tokens_per_day: 1000000000
|
|
28
|
+
|
|
29
|
+
kimi-k2-0711-preview:
|
|
30
|
+
requests_per_minute: 10000
|
|
31
|
+
requests_per_hour: 100000
|
|
32
|
+
requests_per_day: 1000000
|
|
33
|
+
tokens_per_minute: 10000000
|
|
34
|
+
tokens_per_hour: 100000000
|
|
35
|
+
tokens_per_day: 1000000000
|
|
36
|
+
|
|
37
|
+
kimi-k2-turbo-preview:
|
|
38
|
+
requests_per_minute: 10000
|
|
39
|
+
requests_per_hour: 100000
|
|
40
|
+
requests_per_day: 1000000
|
|
41
|
+
tokens_per_minute: 10000000
|
|
42
|
+
tokens_per_hour: 100000000
|
|
43
|
+
tokens_per_day: 1000000000
|
|
44
|
+
|
|
45
|
+
kimi-k2-thinking:
|
|
46
|
+
requests_per_minute: 10000
|
|
47
|
+
requests_per_hour: 100000
|
|
48
|
+
requests_per_day: 1000000
|
|
49
|
+
tokens_per_minute: 10000000
|
|
50
|
+
tokens_per_hour: 100000000
|
|
51
|
+
tokens_per_day: 1000000000
|
|
52
|
+
|
|
53
|
+
kimi-k2-thinking-turbo:
|
|
54
|
+
requests_per_minute: 10000
|
|
55
|
+
requests_per_hour: 100000
|
|
56
|
+
requests_per_day: 1000000
|
|
57
|
+
tokens_per_minute: 10000000
|
|
58
|
+
tokens_per_hour: 100000000
|
|
59
|
+
tokens_per_day: 1000000000
|
|
@@ -15,13 +15,15 @@ class ModelDict(TypedDict):
|
|
|
15
15
|
gemini: Any
|
|
16
16
|
openrouter: Any
|
|
17
17
|
cohere: Any
|
|
18
|
+
moonshot: Any
|
|
18
19
|
|
|
19
20
|
PROVIDER_STRATEGIES: ModelDict = {
|
|
20
21
|
'cerebras': RateLimitStrategy.PER_MODEL,
|
|
21
22
|
'groq': RateLimitStrategy.PER_MODEL,
|
|
22
23
|
'gemini': RateLimitStrategy.PER_MODEL,
|
|
23
24
|
'openrouter': RateLimitStrategy.GLOBAL,
|
|
24
|
-
'cohere': RateLimitStrategy.PER_MODEL
|
|
25
|
+
'cohere': RateLimitStrategy.PER_MODEL,
|
|
26
|
+
'moonshot': RateLimitStrategy.PER_MODEL
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
# Load Cohere tiers first to handle the env var logic
|
|
@@ -41,7 +43,8 @@ MODEL_LIMITS: ModelDict = {
|
|
|
41
43
|
'cohere':{
|
|
42
44
|
# Same for every model
|
|
43
45
|
'default': COHERE_TIERS.get(os.getenv('COHERE_TIER', 'free').lower(), COHERE_TIERS['free'])
|
|
44
|
-
}
|
|
46
|
+
},
|
|
47
|
+
'moonshot': load_rate_limits_from_yaml(os.path.join(MODELS_DIR, 'moonshot.yaml'))
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
OPENROUTER_MODELS = load_openrouter_models(os.path.join(MODELS_DIR, 'openrouter_models.yaml'))
|
|
@@ -14,7 +14,17 @@ from ..config.constants import (
|
|
|
14
14
|
)
|
|
15
15
|
from ..core.utils import is_rate_limit_error, is_temporary_rate_limit_error, get_key_suffix
|
|
16
16
|
from ..core.backoff import ExponentialBackoff, BackoffConfig
|
|
17
|
-
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from agno.models.response import ModelResponse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _UsageResponse:
|
|
22
|
+
"""Minimal response holder for usage recording when agno is not available."""
|
|
23
|
+
__slots__ = ('response_usage',)
|
|
24
|
+
|
|
25
|
+
def __init__(self, usage=None):
|
|
26
|
+
self.response_usage = usage
|
|
27
|
+
|
|
18
28
|
|
|
19
29
|
class RotatingCredentialsMixin:
|
|
20
30
|
"""
|
|
@@ -100,7 +110,7 @@ class RotatingCredentialsMixin:
|
|
|
100
110
|
def _get_retry_limit(self) -> int:
|
|
101
111
|
return min(self._max_retries, len(self.wrapper.manager.keys) - 1)
|
|
102
112
|
|
|
103
|
-
def _record_usage(self, key_obj: KeyUsage, response: Optional[ModelResponse]):
|
|
113
|
+
def _record_usage(self, key_obj: KeyUsage, response: Optional["ModelResponse"]):
|
|
104
114
|
"""
|
|
105
115
|
Extracts usage from the response and reports it to the manager.
|
|
106
116
|
Falls back to estimated_tokens if response is None or usage is missing.
|
|
@@ -132,7 +142,7 @@ class RotatingCredentialsMixin:
|
|
|
132
142
|
multiplier=TEMP_RATE_LIMIT_MULTIPLIER,
|
|
133
143
|
))
|
|
134
144
|
|
|
135
|
-
def invoke(self, *args, **kwargs) -> ModelResponse:
|
|
145
|
+
def invoke(self, *args, **kwargs) -> "ModelResponse":
|
|
136
146
|
limit = self._get_retry_limit()
|
|
137
147
|
|
|
138
148
|
for attempt in range(limit + 1):
|
|
@@ -178,7 +188,7 @@ class RotatingCredentialsMixin:
|
|
|
178
188
|
continue
|
|
179
189
|
raise
|
|
180
190
|
|
|
181
|
-
async def ainvoke(self, *args, **kwargs) -> ModelResponse:
|
|
191
|
+
async def ainvoke(self, *args, **kwargs) -> "ModelResponse":
|
|
182
192
|
limit = self._get_retry_limit()
|
|
183
193
|
|
|
184
194
|
for attempt in range(limit + 1):
|
|
@@ -224,7 +234,7 @@ class RotatingCredentialsMixin:
|
|
|
224
234
|
continue
|
|
225
235
|
raise
|
|
226
236
|
|
|
227
|
-
def invoke_stream(self, *args, **kwargs) -> Iterator[ModelResponse]:
|
|
237
|
+
def invoke_stream(self, *args, **kwargs) -> Iterator["ModelResponse"]:
|
|
228
238
|
limit = self._get_retry_limit()
|
|
229
239
|
|
|
230
240
|
for attempt in range(limit + 1):
|
|
@@ -242,8 +252,7 @@ class RotatingCredentialsMixin:
|
|
|
242
252
|
final_usage = chunk.response_usage
|
|
243
253
|
yield chunk
|
|
244
254
|
if final_usage:
|
|
245
|
-
dummy_response =
|
|
246
|
-
dummy_response.response_usage = final_usage
|
|
255
|
+
dummy_response = _UsageResponse(final_usage)
|
|
247
256
|
self._record_usage(key_usage, dummy_response)
|
|
248
257
|
else:
|
|
249
258
|
# If the stream didn't return usage data, fallback to estimation
|
|
@@ -280,7 +289,7 @@ class RotatingCredentialsMixin:
|
|
|
280
289
|
continue
|
|
281
290
|
raise
|
|
282
291
|
|
|
283
|
-
async def ainvoke_stream(self, *args, **kwargs) -> AsyncIterator[ModelResponse]:
|
|
292
|
+
async def ainvoke_stream(self, *args, **kwargs) -> AsyncIterator["ModelResponse"]:
|
|
284
293
|
limit = self._get_retry_limit()
|
|
285
294
|
|
|
286
295
|
for attempt in range(limit + 1):
|
|
@@ -301,8 +310,7 @@ class RotatingCredentialsMixin:
|
|
|
301
310
|
|
|
302
311
|
# Stream completed successfully. Record usage.
|
|
303
312
|
if final_usage:
|
|
304
|
-
dummy_response =
|
|
305
|
-
dummy_response.response_usage = final_usage
|
|
313
|
+
dummy_response = _UsageResponse(final_usage)
|
|
306
314
|
self._record_usage(key_usage, dummy_response)
|
|
307
315
|
else:
|
|
308
316
|
self._record_usage(key_usage, None)
|
|
@@ -12,7 +12,8 @@ def get_agno_model_class(provider: str):
|
|
|
12
12
|
"gemini": "Gemini",
|
|
13
13
|
"azure": "AzureOpenAI",
|
|
14
14
|
"aws": "Bedrock",
|
|
15
|
-
"openrouter": "OpenRouter"
|
|
15
|
+
"openrouter": "OpenRouter",
|
|
16
|
+
"moonshot": "MoonShot"
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
class_name = overrides.get(p_low, p_low.capitalize())
|
|
@@ -24,6 +24,7 @@ keycycle/config/models/cerebras.yaml
|
|
|
24
24
|
keycycle/config/models/cohere.yaml
|
|
25
25
|
keycycle/config/models/gemini.yaml
|
|
26
26
|
keycycle/config/models/groq.yaml
|
|
27
|
+
keycycle/config/models/moonshot.yaml
|
|
27
28
|
keycycle/config/models/openrouter.yaml
|
|
28
29
|
keycycle/config/models/openrouter_models.yaml
|
|
29
30
|
keycycle/core/__init__.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "keycycle"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="William Jin", email="jinwilliam.jin@gmail.com" },
|
|
10
10
|
]
|
|
@@ -117,5 +117,6 @@ module = [
|
|
|
117
117
|
"groq.*",
|
|
118
118
|
"cerebras.*",
|
|
119
119
|
"openrouter.*",
|
|
120
|
+
"moonshot.*",
|
|
120
121
|
]
|
|
121
122
|
ignore_missing_imports = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|