keycycle 0.3.1__tar.gz → 0.3.2__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.3.1 → keycycle-0.3.2}/PKG-INFO +1 -1
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/utils.py +21 -14
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/PKG-INFO +1 -1
- {keycycle-0.3.1 → keycycle-0.3.2}/pyproject.toml +1 -1
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_multi_client_wrapper.py +23 -11
- {keycycle-0.3.1 → keycycle-0.3.2}/MANIFEST.in +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/__init__.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/adapters/__init__.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/adapters/generic_adapter.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/adapters/openai_adapter.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/__init__.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/constants.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/dataclasses.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/enums.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/loader.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/log_config.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/cerebras.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/cohere.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/gemini.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/groq.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/mistral.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/moonshot.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/openrouter.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/openrouter_models.yaml +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/__init__.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/backoff.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/exceptions.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/key_rotation/__init__.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/key_rotation/rotating_mixin.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/key_rotation/rotation_manager.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/legacy_multi_provider_wrapper.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/multi_client_wrapper.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/py.typed +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/usage/__init__.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/usage/db_logic.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/usage/usage_logger.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/utils.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/SOURCES.txt +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/dependency_links.txt +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/requires.txt +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/top_level.txt +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/setup.cfg +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_api_key_pinning.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_api_key_retrieval.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_cerebras_direct.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_custom_key_limits.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_payment_required_rotation.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_rate_limit_logic.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_specific_key_retrieval.py +0 -0
- {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_streams_recording.py +0 -0
|
@@ -323,16 +323,14 @@ def load_api_keys(
|
|
|
323
323
|
except ValueError:
|
|
324
324
|
raise ValueError(f"'{num_keys_var}' must be an integer, got: {num_keys_raw}")
|
|
325
325
|
|
|
326
|
-
#
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
# order the indices fall, and use all of them -- NUM_{PROVIDER} becomes
|
|
335
|
-
# an expectation to warn against, not a hard loop bound.
|
|
326
|
+
# NUM_{PROVIDER} is AUTHORITATIVE (0.3.2, reverting 0.3.1's scan-all):
|
|
327
|
+
# the fleet convention is that the first NUM indices (sorted ascending)
|
|
328
|
+
# are the sanctioned live keys, and keys parked at higher indices beyond
|
|
329
|
+
# a numbering gap are dead or spare on purpose -- loading them would put
|
|
330
|
+
# known-bad keys back into rotation. Gaps INSIDE the sanctioned range
|
|
331
|
+
# are tolerated (a retired key's index can be missing); the loader takes
|
|
332
|
+
# the first NUM indices that actually exist and warns if it comes up
|
|
333
|
+
# short, but never loads past NUM keys.
|
|
336
334
|
key_pattern = re.compile(rf"^{re.escape(provider_upper)}_API_KEY_(\d+)$")
|
|
337
335
|
found: Dict[int, str] = {}
|
|
338
336
|
for env_name, value in os.environ.items():
|
|
@@ -343,11 +341,20 @@ def load_api_keys(
|
|
|
343
341
|
if not found:
|
|
344
342
|
raise ValueError(f"Missing API key: {provider_upper}_API_KEY_1")
|
|
345
343
|
|
|
346
|
-
|
|
347
|
-
|
|
344
|
+
all_indices = sorted(found)
|
|
345
|
+
indices = all_indices[:expected_count]
|
|
346
|
+
excluded = all_indices[expected_count:]
|
|
347
|
+
if excluded:
|
|
348
|
+
logging.getLogger(__name__).info(
|
|
349
|
+
"%s: %s=%d -- using the first %d key indices %s; excluding %d "
|
|
350
|
+
"parked key(s) at indices %s (dead/spare by convention).",
|
|
351
|
+
provider_upper, num_keys_var, expected_count, len(indices), indices,
|
|
352
|
+
len(excluded), excluded,
|
|
353
|
+
)
|
|
354
|
+
if len(indices) < expected_count:
|
|
348
355
|
logging.getLogger(__name__).warning(
|
|
349
|
-
"%s:
|
|
350
|
-
"
|
|
356
|
+
"%s: only %d API key(s) found (indices %s) but %s=%d -- the "
|
|
357
|
+
"declared count is stale. Using the %d key(s) actually found.",
|
|
351
358
|
provider_upper, len(indices), indices, num_keys_var, expected_count,
|
|
352
359
|
len(indices),
|
|
353
360
|
)
|
|
@@ -331,25 +331,37 @@ class TestLoadApiKeysStatic(unittest.TestCase):
|
|
|
331
331
|
self.assertIn("Missing API key", str(ctx.exception))
|
|
332
332
|
|
|
333
333
|
@patch.dict(os.environ, {
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
334
|
+
# The fleet convention (0.3.2): the first NUM indices are the
|
|
335
|
+
# sanctioned live keys; keys parked at higher indices beyond a gap
|
|
336
|
+
# are dead or spare ON PURPOSE (mirrors William's GROQ fleet:
|
|
337
|
+
# 1..24 live, 37..46 parked dead). NUM is authoritative.
|
|
337
338
|
"NUM_GAPPY": "24",
|
|
338
339
|
**{f"GAPPY_API_KEY_{i}": f"key-{i}" for i in range(1, 25)},
|
|
339
340
|
**{f"GAPPY_API_KEY_{i}": f"key-{i}" for i in range(37, 47)},
|
|
340
341
|
})
|
|
341
342
|
@patch('keycycle.multi_client_wrapper.load_dotenv')
|
|
342
|
-
def
|
|
343
|
-
"""
|
|
343
|
+
def test_load_keys_num_caps_out_parked_keys(self, mock_dotenv):
|
|
344
|
+
"""Only the first NUM sorted indices load; parked keys stay out."""
|
|
344
345
|
keys = MultiClientWrapper.load_api_keys("gappy")
|
|
345
346
|
|
|
346
|
-
|
|
347
|
-
# NUM_GAPPY only claims 24 -- the env var is a hint, not a cap.
|
|
348
|
-
self.assertEqual(len(keys), 34)
|
|
347
|
+
self.assertEqual(len(keys), 24)
|
|
349
348
|
self.assertEqual(keys[0], "key-1")
|
|
350
|
-
self.assertEqual(keys[
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
self.assertEqual(keys[-1], "key-24")
|
|
350
|
+
|
|
351
|
+
@patch.dict(os.environ, {
|
|
352
|
+
# A gap INSIDE the sanctioned range is tolerated: index 2 retired,
|
|
353
|
+
# NUM still says 3, so the loader takes the first 3 that exist
|
|
354
|
+
# (1, 3, 4) rather than crashing or stopping at the gap.
|
|
355
|
+
"NUM_HOLEY": "3",
|
|
356
|
+
"HOLEY_API_KEY_1": "key-1",
|
|
357
|
+
"HOLEY_API_KEY_3": "key-3",
|
|
358
|
+
"HOLEY_API_KEY_4": "key-4",
|
|
359
|
+
"HOLEY_API_KEY_9": "key-9",
|
|
360
|
+
})
|
|
361
|
+
@patch('keycycle.multi_client_wrapper.load_dotenv')
|
|
362
|
+
def test_load_keys_tolerates_gap_inside_sanctioned_range(self, mock_dotenv):
|
|
363
|
+
keys = MultiClientWrapper.load_api_keys("holey")
|
|
364
|
+
self.assertEqual(keys, ["key-1", "key-3", "key-4"])
|
|
353
365
|
|
|
354
366
|
@patch.dict(os.environ, {
|
|
355
367
|
"NUM_SPARSE": "5",
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|