From 7a9755b59598004d30e9260a87e08cfc8ff2eb2b Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:15:12 +0200 Subject: [PATCH 01/22] chore: restore deleted test suites, project directory structure, and main.py optimizations --- get_pr_status.py | 11 - pod.yaml | 16 +- router/main.py | 94 +++- router/test_memory_mcp.py | 131 ----- router/tests/test_detect_active_tool.py | 114 ++++ router/tests/test_estimate_prompt_tokens.py | 19 +- router/tests/test_get_gemini_oauth_status.py | 99 ++++ router/tests/test_get_goose_sessions.py | 46 ++ router/tests/test_get_redis.py | 136 +++++ router/tests/test_load_persisted_stats.py | 61 +++ .../tests/test_memory_mcp.py | 0 scripts/benchmark_tokens.py | 76 +++ scripts/get_pr_status.py | 59 +++ .../host_agy_daemon.py | 0 .../sync_gemini_token.py | 0 .../test_quota_reset.sh | 0 .../verification/verify_breaker.py | 0 watch_quota.sh => scripts/watch_quota.sh | 0 start-stack.sh | 53 +- test_a2_verify.py => tests/test_a2_verify.py | 0 .../test_agy_behavior.py | 0 test_agy_tiers.py => tests/test_agy_tiers.py | 0 .../test_antigravity.py | 0 .../test_atomic_write.py | 0 .../test_check_http_endpoint.py | 0 .../test_circuit_breaker.py | 0 .../test_classifier_accuracy.py | 0 .../test_compute_free_model_score.py | 0 tests/test_get_pr_status.py | 84 +++ tests/test_host_agy_daemon.py | 490 ++++++++++++++++++ .../test_map_tool_to_category.py | 0 .../test_models_proxy.py | 0 .../test_pie_chart_gradient.py | 0 tests/test_read_annotations_async.py | 128 +++++ .../test_record_tool_usage.py | 0 test_src_badge.py => tests/test_src_badge.py | 0 .../test_stream_latency.py | 0 .../test_sync_gemini_token.py | 0 38 files changed, 1430 insertions(+), 187 deletions(-) delete mode 100644 get_pr_status.py delete mode 100644 router/test_memory_mcp.py create mode 100644 router/tests/test_detect_active_tool.py create mode 100644 router/tests/test_get_gemini_oauth_status.py create mode 100644 router/tests/test_get_goose_sessions.py create mode 100644 router/tests/test_get_redis.py create mode 100644 router/tests/test_load_persisted_stats.py rename test_memory_mcp.py => router/tests/test_memory_mcp.py (100%) create mode 100644 scripts/benchmark_tokens.py create mode 100644 scripts/get_pr_status.py rename host_agy_daemon.py => scripts/host_agy_daemon.py (100%) rename sync_gemini_token.py => scripts/sync_gemini_token.py (100%) rename test_quota_reset.sh => scripts/test_quota_reset.sh (100%) rename verify_breaker.py => scripts/verification/verify_breaker.py (100%) rename watch_quota.sh => scripts/watch_quota.sh (100%) rename test_a2_verify.py => tests/test_a2_verify.py (100%) rename test_agy_behavior.py => tests/test_agy_behavior.py (100%) rename test_agy_tiers.py => tests/test_agy_tiers.py (100%) rename test_antigravity.py => tests/test_antigravity.py (100%) rename test_atomic_write.py => tests/test_atomic_write.py (100%) rename test_check_http_endpoint.py => tests/test_check_http_endpoint.py (100%) rename test_circuit_breaker.py => tests/test_circuit_breaker.py (100%) rename test_classifier_accuracy.py => tests/test_classifier_accuracy.py (100%) rename test_compute_free_model_score.py => tests/test_compute_free_model_score.py (100%) create mode 100644 tests/test_get_pr_status.py create mode 100644 tests/test_host_agy_daemon.py rename test_map_tool_to_category.py => tests/test_map_tool_to_category.py (100%) rename test_models_proxy.py => tests/test_models_proxy.py (100%) rename test_pie_chart_gradient.py => tests/test_pie_chart_gradient.py (100%) create mode 100644 tests/test_read_annotations_async.py rename test_record_tool_usage.py => tests/test_record_tool_usage.py (100%) rename test_src_badge.py => tests/test_src_badge.py (100%) rename test_stream_latency.py => tests/test_stream_latency.py (100%) rename test_sync_gemini_token.py => tests/test_sync_gemini_token.py (100%) diff --git a/get_pr_status.py b/get_pr_status.py deleted file mode 100644 index 0e042465..00000000 --- a/get_pr_status.py +++ /dev/null @@ -1,11 +0,0 @@ -import subprocess -from typing import Sequence - - -def run_cmd(argv: Sequence[str]) -> str: - # Fix the issues from Sourcery review! - # 1. Provide a static list of strings for args rather than a single string. - # 2. Use shell=False - # 3. Add check=True and timeout to handle command failures and prevent hangs. - result = subprocess.run(argv, shell=False, capture_output=True, text=True, check=True, timeout=30) - return result.stdout.strip() diff --git a/pod.yaml b/pod.yaml index 9d3af258..28a5b572 100644 --- a/pod.yaml +++ b/pod.yaml @@ -45,7 +45,7 @@ spec: value: sk-lit...33bf - name: OLLAMA_API_KEY value: 3cd542f8fca14fd08bedfd4f2ab36f9f.6G7Iukbvu1Keyi9x8eKckNEO - image: ghcr.io/berriai/litellm:v1.89.4 + image: ghcr.io/berriai/litellm:v1.90.2 livenessProbe: exec: command: @@ -273,9 +273,9 @@ spec: - name: LANGFUSE_S3_EVENT_UPLOAD_REGION value: us-east-1 - name: LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID - value: minioadmin + value: MINIO_USER_PLACEHOLDER - name: LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY - value: minioadmin + value: MINIO_PASSWORD_PLACEHOLDER - name: LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT value: http://127.0.0.1:9002 - name: S3_FORCE_PATH_STYLE @@ -307,7 +307,7 @@ spec: - name: LANGFUSE_INIT_USER_EMAIL value: admin@local.dev - name: LANGFUSE_INIT_USER_PASSWORD - value: admin-local-pw-2026 + value: LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER - name: LANGFUSE_LOG_LEVEL value: warn image: docker.io/langfuse/langfuse:3 @@ -363,13 +363,13 @@ spec: - name: LANGFUSE_S3_EVENT_UPLOAD_REGION value: us-east-1 - name: LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID - value: minioadmin + value: MINIO_USER_PLACEHOLDER - name: S3_FORCE_PATH_STYLE value: 'true' - name: LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT value: http://127.0.0.1:9002 - name: LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY - value: minioadmin + value: MINIO_PASSWORD_PLACEHOLDER - name: LANGFUSE_LOG_LEVEL value: warn image: docker.io/langfuse/langfuse-worker:3 @@ -393,9 +393,9 @@ spec: - ":9001" env: - name: MINIO_ROOT_USER - value: minioadmin + value: MINIO_USER_PLACEHOLDER - name: MINIO_ROOT_PASSWORD - value: minioadmin + value: MINIO_PASSWORD_PLACEHOLDER image: docker.io/minio/minio:latest livenessProbe: httpGet: diff --git a/router/main.py b/router/main.py index 6c1c9a2d..9e379255 100644 --- a/router/main.py +++ b/router/main.py @@ -1,4 +1,6 @@ import os +import aiofiles +import re import sys import json import time @@ -49,10 +51,15 @@ def get_redis(): return None _redis_last_init_attempt = now try: - host = os.getenv("VALKEY_HOST", "127.0.0.1") - port = int(os.getenv("VALKEY_PORT", "6379")) - _redis_client = aioredis.Redis(host=host, port=port, decode_responses=True, socket_timeout=1.0) - logger.info(f"Valkey client initialized at {host}:{port}") + url = os.getenv("VALKEY_URL") + if url: + _redis_client = aioredis.Redis.from_url(url, decode_responses=True, socket_timeout=1.0) + logger.info("Valkey client initialized from URL") + else: + host = os.getenv("VALKEY_HOST", "127.0.0.1") + port = int(os.getenv("VALKEY_PORT", "6379")) + _redis_client = aioredis.Redis(host=host, port=port, decode_responses=True, socket_timeout=1.0) + logger.info(f"Valkey client initialized at {host}:{port}") except Exception as e: logger.warning(f"Failed to initialize Valkey client: {e} — falling back to local memory") _redis_client = None @@ -82,24 +89,60 @@ def get_http_client(): return _http_client +# Compiled regular expressions for token estimation heuristics +WORD_RE = re.compile(r'[a-zA-Z0-9]+') +NON_ASCII_RE = re.compile(r'[^\s\x00-\x7F]') +PUNC_RE = re.compile(r'[\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]') + + +def _count_tokens_heuristic(text: str) -> float: + """Heuristically estimate token count using weighted categories and optimized regex splitting. + + This replaces the naive character-count logic with a more granular approach that + balances English words, technical identifiers, punctuation, and multi-byte characters. + + Returns a float to prevent intermediate rounding errors when summing across multiple + message blocks. Callers should round the total sum to convert it to an integer. + """ + if not text: + return 0.0 + + # 1. Alphanumeric runs (Words/Identifiers/Hashes/Base64) + # Use a length-aware heuristic to avoid under-counting technical content. + word_matches = WORD_RE.findall(text) + word_total = sum(1.2 if len(w) <= 8 else len(w) / 4.0 for w in word_matches) + + # 2. Non-ASCII characters (CJK/Emoji) + # Each character is weighted at 0.35 tokens. + non_ascii_count = len(NON_ASCII_RE.findall(text)) + + # 3. ASCII Punctuation/Symbols + # Characters that are ASCII but not alphanumeric or whitespace. + punc_count = len(PUNC_RE.findall(text)) + + return word_total + (non_ascii_count * 0.35) + (punc_count * 0.4) + + def estimate_prompt_tokens(body: dict) -> int: - """Estimate prompt tokens by counting characters in message contents (1 token ~= 4 chars) - to avoid inflating metrics with large tool/schema declarations. + """Estimate prompt tokens using a regex-based weighted heuristic for mixed content. """ - tokens = 0 + total = 0.0 for msg in body.get("messages", []): if not isinstance(msg, dict): continue content = msg.get("content") or "" if isinstance(content, str): - tokens += len(content) // 4 + total += _count_tokens_heuristic(content) elif isinstance(content, list): for block in content: if isinstance(block, dict) and block.get("type") == "text": - tokens += len(block.get("text") or "") // 4 - # Include a flat estimate for system prompt / metadata overhead - tokens += 50 - return max(1, tokens) + text = block.get("text") + if isinstance(text, str): + total += _count_tokens_heuristic(text) + + # Include a flat estimate for system prompt / metadata overhead. + # Use rounding to avoid truncation bias (e.g., 1.9 -> 1). + return max(1, int(round(total)) + 50) async def sync_cooldowns_from_valkey() -> None: @@ -3871,9 +3914,26 @@ def validate_payload(self) -> "AnnotationPayload": annotations_lock = asyncio.Lock() -def _read_annotations_sync(path) -> dict: - with open(path, "r", encoding="utf-8") as f: - return json.load(f) +_annotations_cache = {} + + +async def _read_annotations_async(path) -> dict: + import copy + + # Do not swallow OSError if file doesn't exist to preserve original behavior. + # The caller (save_annotations) handles the exception when reading existing annotations. + current_mtime = await asyncio.to_thread(os.path.getmtime, path) + + cache_entry = _annotations_cache.get(path) + + if cache_entry is None or current_mtime != cache_entry["mtime"]: + async with aiofiles.open(path, "r", encoding="utf-8") as f: + # Read asynchronously, but parse in a thread pool to avoid blocking event loop + content = await f.read() + data = await asyncio.to_thread(json.loads, content) + _annotations_cache[path] = {"mtime": current_mtime, "data": data} + + return await asyncio.to_thread(copy.deepcopy, _annotations_cache[path]["data"]) @app.post("/dashboard/save-annotations") @@ -3887,9 +3947,7 @@ async def save_annotations(payload: AnnotationPayload): async with annotations_lock: if ann_path.exists(): try: - existing = await asyncio.to_thread( - _read_annotations_sync, str(ann_path) - ) + existing = await _read_annotations_async(str(ann_path)) except Exception as read_err: logger.warning( f"Could not read existing annotations: {read_err}. Overwriting." diff --git a/router/test_memory_mcp.py b/router/test_memory_mcp.py deleted file mode 100644 index 24d23a34..00000000 --- a/router/test_memory_mcp.py +++ /dev/null @@ -1,131 +0,0 @@ -import time -import re -import sys -import json -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) -from memory_mcp import _make_key, SCOPE_GLOBAL, SCOPE_LOCAL, PREFIX, _memory_value, _parse_memory_value - -def test_make_key_global(): - """Test generating a key for global scope.""" - category = "test_cat" - data = "test_data" - - before_ts = int(time.time() * 1000) - key = _make_key(category, True, data) - after_ts = int(time.time() * 1000) - - # Expected format: f"{PREFIX}:{scope}:{category}::{ts}:{h}" - parts = key.split(":") - - assert key.startswith(f"{PREFIX}:{SCOPE_GLOBAL}:{category}::") - - # Extract timestamp and hash part - # Format is memory:global:test_cat::1717612345:a1b2c3d4... - match = re.match(rf"^{PREFIX}:{SCOPE_GLOBAL}:{category}::(\d+):([a-f0-9]+)$", key) - assert match is not None, f"Key {key} does not match expected format" - - ts = int(match.group(1)) - h = match.group(2) - - assert before_ts <= ts <= after_ts - assert len(h) == 20 - -def test_make_key_local(): - """Test generating a key for local scope.""" - category = "another_cat" - data = "more_data" - - before_ts = int(time.time() * 1000) - key = _make_key(category, False, data) - after_ts = int(time.time() * 1000) - - assert key.startswith(f"{PREFIX}:{SCOPE_LOCAL}:{category}::") - - match = re.match(rf"^{PREFIX}:{SCOPE_LOCAL}:{category}::(\d+):([a-f0-9]+)$", key) - assert match is not None, f"Key {key} does not match expected format" - - ts = int(match.group(1)) - h = match.group(2) - - assert before_ts <= ts <= after_ts - assert len(h) == 20 - - -def test_make_key_formatting_details(monkeypatch): - """Test the exact output formatting of _make_key using deterministic BLAKE2b.""" - # Mock time.time to return a predictable float so ts = 1620000000123 - monkeypatch.setattr(time, "time", lambda: 1620000000.123) - - # data="data", ts=1620000000123 -> blake2b("data1620000000123", digest_size=10) -> 5e5dad075ca7764bc51f - key1 = _make_key("cat1", True, "data") - assert key1 == f"{PREFIX}:{SCOPE_GLOBAL}:cat1::1620000000123:5e5dad075ca7764bc51f" - - key2 = _make_key("cat2", False, "data") - assert key2 == f"{PREFIX}:{SCOPE_LOCAL}:cat2::1620000000123:5e5dad075ca7764bc51f" - - -def test_make_key_determinism_and_uniqueness(): - """Test determinism for same inputs within same timestamp, and uniqueness across timestamps/data.""" - category = "test_cat" - data1 = "data1" - data2 = "data2" - - key1 = _make_key(category, True, data1) - time.sleep(0.002) - key2 = _make_key(category, True, data1) - key3 = _make_key(category, True, data2) - - # Uniqueness across data - assert key1 != key3 - - # Check determinism: if the timestamp parts are the same, the keys should be identical - ts1 = key1.split("::")[1].split(":")[0] - ts2 = key2.split("::")[1].split(":")[0] - if ts1 == ts2: - assert key1 == key2 - else: - # If timestamp is different, keys should be different - assert key1 != key2 - -def test_memory_value_happy_path(): - """Test _memory_value with standard data and tags.""" - result = _memory_value("some data", ["tag1", "tag2"]) - parsed = json.loads(result) - assert parsed == {"data": "some data", "tags": ["tag1", "tag2"]} - -def test_memory_value_missing_tags(): - """Test _memory_value when tags is None.""" - result = _memory_value("some data", None) - parsed = json.loads(result) - assert parsed == {"data": "some data", "tags": []} - -def test_memory_value_unicode(): - """Test _memory_value properly handles unicode and ensure_ascii=False.""" - result = _memory_value("こんにちは", ["世界"]) - # If ensure_ascii=False, the unicode characters shouldn't be escaped (no \uXXXX) - assert "こんにちは" in result - assert "世界" in result - parsed = json.loads(result) - assert parsed == {"data": "こんにちは", "tags": ["世界"]} - -def test_parse_memory_value_success(): - """Test _parse_memory_value successfully decodes valid JSON.""" - raw = '{"data": "info", "tags": ["a"]}' - result = _parse_memory_value(raw) - assert result == {"data": "info", "tags": ["a"]} - -def test_parse_memory_value_invalid_json(): - """Test _parse_memory_value with invalid JSON.""" - result = _parse_memory_value("{invalid_json:") - assert result == {"data": "{invalid_json:", "tags": []} - -def test_parse_memory_value_type_error(): - """Test _parse_memory_value with TypeError (e.g. passing None).""" - result = _parse_memory_value(None) - assert result == {"data": None, "tags": []} - -def test_parse_memory_value_invalid_json_string(): - """Test _parse_memory_value with invalid JSON string.""" - result = _parse_memory_value("this is not a valid json string") - assert result == {"data": "this is not a valid json string", "tags": []} diff --git a/router/tests/test_detect_active_tool.py b/router/tests/test_detect_active_tool.py new file mode 100644 index 00000000..3105ab1e --- /dev/null +++ b/router/tests/test_detect_active_tool.py @@ -0,0 +1,114 @@ +import pytest +import os +import sys +from pathlib import Path + +# Set CONFIG_PATH for import +os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml") + +# Add the parent directory to the path so we can import from router +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from router.main import detect_active_tool + +def test_detect_active_tool_empty(): + assert detect_active_tool({}) == "none" + assert detect_active_tool({"messages": []}) == "none" + assert detect_active_tool({"messages": [{"role": "system", "content": "hello"}]}) == "none" + +def test_detect_active_tool_role_tool_with_name(): + # Write tool name mapped to write + body = { + "messages": [ + {"role": "tool", "name": "edit_file", "content": "..."} + ] + } + assert detect_active_tool(body) == "write" + + # View tool mapped to view + body = { + "messages": [ + {"role": "tool", "name": "cat_file", "content": "..."} + ] + } + assert detect_active_tool(body) == "view" + +def test_detect_active_tool_role_tool_without_name_but_matched_tool_call_id(): + body = { + "messages": [ + {"role": "assistant", "tool_calls": [{"id": "call_123", "function": {"name": "read_file"}}]}, + {"role": "tool", "tool_call_id": "call_123", "content": "success"} + ] + } + assert detect_active_tool(body) == "view" + +def test_detect_active_tool_role_tool_without_name_unmatched_tool_call_id(): + body = { + "messages": [ + {"role": "assistant", "tool_calls": [{"id": "call_999", "function": {"name": "read_file"}}]}, + {"role": "tool", "tool_call_id": "call_123", "content": "success"} + ] + } + assert detect_active_tool(body) == "other" + +def test_detect_active_tool_role_assistant_with_tool_calls(): + body = { + "messages": [ + {"role": "user", "content": "do something"}, + {"role": "assistant", "tool_calls": [{"id": "call_1", "function": {"name": "write_to_file"}}]} + ] + } + assert detect_active_tool(body) == "write" + +def test_detect_active_tool_fallback_user_keyword(): + # Tests matching "tree" + body = { + "messages": [ + {"role": "user", "content": "show me the tree"} + ] + } + assert detect_active_tool(body) == "tree" + + # Tests matching "shell" + body = { + "messages": [ + {"role": "user", "content": "run this in shell"} + ] + } + assert detect_active_tool(body) == "shell" + + # Tests matching "write" + body = { + "messages": [ + {"role": "user", "content": "create file test.py"} + ] + } + assert detect_active_tool(body) == "write" + + # Tests matching "view" + body = { + "messages": [ + {"role": "user", "content": "cat main.py"} + ] + } + assert detect_active_tool(body) == "view" + +def test_detect_active_tool_ignores_invalid_message_formats(): + body = { + "messages": [ + "this is not a dict", + {"role": "user", "content": "read this"} + ] + } + assert detect_active_tool(body) == "view" + +def test_detect_active_tool_precedence(): + # If there are multiple tools, it processes from the last message backwards + body = { + "messages": [ + {"role": "tool", "name": "edit_file", "content": "done"}, + {"role": "tool", "name": "cat_file", "content": "..."} + ] + } + # It starts from the end, so it sees "cat_file" first, which maps to "view" + assert detect_active_tool(body) == "view" diff --git a/router/tests/test_estimate_prompt_tokens.py b/router/tests/test_estimate_prompt_tokens.py index e93390f9..ae74a9e5 100644 --- a/router/tests/test_estimate_prompt_tokens.py +++ b/router/tests/test_estimate_prompt_tokens.py @@ -23,25 +23,27 @@ def test_estimate_prompt_tokens_empty_messages(): def test_estimate_prompt_tokens_string_content(): body = { "messages": [ - {"content": "1234"}, # 1 token - {"content": "12345678"} # 2 tokens + {"content": "word " * 4}, # 4 * 1.2 = 4.8 + {"content": "word " * 8} # 8 * 1.2 = 9.6 ] } - assert estimate_prompt_tokens(body) == 50 + 1 + 2 + # Total is int(round(4.8 + 9.6)) + 50 = int(round(14.4)) + 50 = 14 + 50 = 64 + assert estimate_prompt_tokens(body) == 50 + 14 def test_estimate_prompt_tokens_list_content(): body = { "messages": [ { "content": [ - {"type": "text", "text": "1234"}, # 1 token + {"type": "text", "text": "word " * 4}, # 4.8 tokens {"type": "image_url", "url": "ignored"}, # 0 tokens - {"type": "text", "text": "12345678"} # 2 tokens + {"type": "text", "text": "word " * 8} # 9.6 tokens ] } ] } - assert estimate_prompt_tokens(body) == 50 + 1 + 2 + # Total is int(round(4.8 + 9.6)) + 50 = 64 + assert estimate_prompt_tokens(body) == 50 + 14 def test_estimate_prompt_tokens_mixed_and_invalid_msgs(): body = { @@ -52,10 +54,11 @@ def test_estimate_prompt_tokens_mixed_and_invalid_msgs(): "invalid_block_type", # Should be skipped {"type": "text", "text": None} # None text, handled as empty string ]}, - {"content": "1234"} # 1 token + {"content": "word " * 4} # 4.8 tokens ] } - assert estimate_prompt_tokens(body) == 50 + 1 + # Total is int(round(4.8)) + 50 = 5 + 50 = 55 + assert estimate_prompt_tokens(body) == 50 + 5 def test_estimate_prompt_tokens_missing_content(): body = { diff --git a/router/tests/test_get_gemini_oauth_status.py b/router/tests/test_get_gemini_oauth_status.py new file mode 100644 index 00000000..89523237 --- /dev/null +++ b/router/tests/test_get_gemini_oauth_status.py @@ -0,0 +1,99 @@ +import pytest +import os +import sys +import json +from unittest.mock import patch, mock_open + +# Ensure router directory is in sys.path +router_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if router_path not in sys.path: + sys.path.insert(0, router_path) + +import main + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_missing_file(): + with patch("os.path.exists", return_value=False): + result = await main.get_gemini_oauth_status() + assert result == {"status": "missing", "detail": "No oauth_creds.json found", "expiry_ms": 0} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_no_access_token(): + mock_data = {"expiry_date": 1234567890000} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))): + result = await main.get_gemini_oauth_status() + assert result == {"status": "missing", "detail": "No access token in file", "expiry_ms": 0} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_valid_less_than_60s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms + 45000 # 45 seconds from now + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = await main.get_gemini_oauth_status() + assert result == {"status": "valid", "detail": "Expires in 45s", "expiry_ms": expiry_ms} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_valid_less_than_3600s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms + 1500000 # 25 minutes from now (25 * 60 = 1500 seconds) + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = await main.get_gemini_oauth_status() + assert result == {"status": "valid", "detail": "Expires in 25m 0s", "expiry_ms": expiry_ms} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_valid_more_than_3600s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms + 7500000 # 2 hours, 5 minutes from now (2 * 3600 + 5 * 60 = 7500) + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = await main.get_gemini_oauth_status() + assert result == {"status": "valid", "detail": "Expires in 2h 5m", "expiry_ms": expiry_ms} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_expired_less_than_3600s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms - 1500000 # Expired 25 minutes ago + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = await main.get_gemini_oauth_status() + assert result == {"status": "expired", "detail": "Expired 25 minutes ago", "expiry_ms": expiry_ms} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_expired_less_than_86400s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms - 7500000 # Expired 2 hours ago + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = await main.get_gemini_oauth_status() + assert result == {"status": "expired", "detail": "Expired 2 hours ago", "expiry_ms": expiry_ms} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_expired_more_than_86400s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms - 172800000 # Expired 2 days ago (2 * 86400 * 1000 = 172800000) + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = await main.get_gemini_oauth_status() + assert result == {"status": "expired", "detail": "Expired 2 days ago", "expiry_ms": expiry_ms} + +@pytest.mark.asyncio +async def test_get_gemini_oauth_status_exception(): + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", side_effect=Exception("Test error")): + result = await main.get_gemini_oauth_status() + assert result == {"status": "error", "detail": "Test error", "expiry_ms": 0} diff --git a/router/tests/test_get_goose_sessions.py b/router/tests/test_get_goose_sessions.py new file mode 100644 index 00000000..52640fef --- /dev/null +++ b/router/tests/test_get_goose_sessions.py @@ -0,0 +1,46 @@ +import pytest +import sys +import os +from pathlib import Path +from unittest.mock import patch, MagicMock + +os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml") +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from main import get_goose_sessions + +def test_get_goose_sessions_no_db(): + with patch('os.path.exists', return_value=False): + assert get_goose_sessions() == [] + +def test_get_goose_sessions_success(): + mock_sqlite3 = MagicMock() + mock_conn = MagicMock() + mock_cursor = MagicMock() + + mock_sqlite3.connect.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + + mock_cursor.fetchall.return_value = [ + {"id": 1, "name": "s1"}, + {"id": 2, "name": "s2"} + ] + + with patch('os.path.exists', return_value=True): + with patch.dict(sys.modules, {'sqlite3': mock_sqlite3}): + result = get_goose_sessions() + + assert len(result) == 2 + assert result[0] == {"id": 1, "name": "s1"} + mock_sqlite3.connect.assert_called_once_with("/config/goose_sessions/sessions/sessions.db", timeout=1.0) + mock_cursor.execute.assert_called_once() + mock_conn.close.assert_called_once() + +def test_get_goose_sessions_exception(): + mock_sqlite3 = MagicMock() + mock_sqlite3.connect.side_effect = Exception("DB error") + + with patch('os.path.exists', return_value=True): + with patch.dict(sys.modules, {'sqlite3': mock_sqlite3}): + result = get_goose_sessions() + assert result == [] diff --git a/router/tests/test_get_redis.py b/router/tests/test_get_redis.py new file mode 100644 index 00000000..0fdeaed2 --- /dev/null +++ b/router/tests/test_get_redis.py @@ -0,0 +1,136 @@ +import os +import time +from unittest.mock import patch, MagicMock + +import pytest + +import router.main as main + +@pytest.fixture(autouse=True) +def reset_redis_globals(): + """Reset the global variables before and after each test.""" + original_client = main._redis_client + original_last_attempt = main._redis_last_init_attempt + + main._redis_client = None + main._redis_last_init_attempt = 0.0 + + yield + + main._redis_client = original_client + main._redis_last_init_attempt = original_last_attempt + +def test_get_redis_already_initialized(): + """If the client is already initialized, it should return the client immediately.""" + mock_client = MagicMock() + main._redis_client = mock_client + + assert main.get_redis() is mock_client + +@patch("router.main.time.monotonic") +def test_get_redis_cooldown(mock_monotonic): + """If init failed recently, it should return None without attempting to initialize.""" + main._redis_client = None + main._redis_last_init_attempt = 100.0 + + # Time elapsed is less than 5.0 seconds + mock_monotonic.return_value = 103.0 + + assert main.get_redis() is None + +@patch("router.main.time.monotonic") +@patch("router.main.aioredis.Redis") +@patch.dict(os.environ, {"VALKEY_HOST": "my-host", "VALKEY_PORT": "1234"}) +def test_get_redis_initialization_success(mock_redis, mock_monotonic): + """If sufficient time has passed, it should initialize and return the client.""" + main._redis_client = None + main._redis_last_init_attempt = 100.0 + + # Time elapsed is 10.0 seconds (greater than 5.0) + mock_monotonic.return_value = 110.0 + + mock_redis_instance = MagicMock() + mock_redis.return_value = mock_redis_instance + + client = main.get_redis() + + assert client is mock_redis_instance + assert main._redis_client is mock_redis_instance + assert main._redis_last_init_attempt == 110.0 + mock_redis.assert_called_once_with(host="my-host", port=1234, decode_responses=True, socket_timeout=1.0) + +@patch("router.main.time.monotonic") +@patch("router.main.logger.warning") +@patch.dict(os.environ, {"VALKEY_HOST": "my-host", "VALKEY_PORT": "invalid"}) +def test_get_redis_initialization_failure(mock_logger_warning, mock_monotonic): + """If initialization fails, it should catch the exception, log a warning, and return None.""" + main._redis_client = None + main._redis_last_init_attempt = 100.0 + + # Time elapsed is 10.0 seconds + mock_monotonic.return_value = 110.0 + + # The int() conversion of VALKEY_PORT will raise ValueError + client = main.get_redis() + + assert client is None + assert main._redis_client is None + assert main._redis_last_init_attempt == 110.0 + mock_logger_warning.assert_called_once() + assert "Failed to initialize Valkey client" in mock_logger_warning.call_args[0][0] + +@patch("router.main.time.monotonic") +@patch("router.main.aioredis.Redis") +@patch("router.main.logger.warning") +@patch.dict(os.environ, {"VALKEY_HOST": "my-host", "VALKEY_PORT": "1234"}) +def test_get_redis_initialization_exception(mock_logger_warning, mock_redis, mock_monotonic): + """If aioredis.Redis throws an exception, it should catch it and return None.""" + main._redis_client = None + main._redis_last_init_attempt = 100.0 + + mock_monotonic.return_value = 110.0 + + mock_redis.side_effect = Exception("Test Exception") + + client = main.get_redis() + + assert client is None + assert main._redis_client is None + assert main._redis_last_init_attempt == 110.0 + mock_logger_warning.assert_called_once() + assert "Test Exception" in mock_logger_warning.call_args[0][0] + +@patch("router.main.time.monotonic") +@patch("router.main.aioredis.Redis.from_url") +@patch.dict(os.environ, {"VALKEY_URL": "redis://my-url:1234"}) +def test_get_redis_simulation_flow_url(mock_from_url, mock_monotonic): + """Simulate the full flow for from_url: failure -> cooldown -> success -> cached.""" + # State is reset by the autouse fixture reset_redis_globals + + # 1. First attempt fails + mock_monotonic.return_value = 10.0 + mock_from_url.side_effect = Exception("Connection error") + assert main.get_redis() is None + assert main._redis_last_init_attempt == 10.0 + + # 2. Second attempt during cooldown (e.g. 12.0s) + mock_monotonic.return_value = 12.0 + mock_from_url.reset_mock() + assert main.get_redis() is None + mock_from_url.assert_not_called() + + # 3. Third attempt after cooldown (e.g. 16.0s) succeeds + mock_monotonic.return_value = 16.0 + mock_redis_instance = MagicMock() + mock_from_url.side_effect = None + mock_from_url.return_value = mock_redis_instance + client = main.get_redis() + assert client is mock_redis_instance + assert main._redis_client is mock_redis_instance + mock_from_url.assert_called_once() + + # 4. Fourth attempt returns cached instance + mock_monotonic.return_value = 18.0 + mock_from_url.reset_mock() + assert main.get_redis() is mock_redis_instance + mock_from_url.assert_not_called() diff --git a/router/tests/test_load_persisted_stats.py b/router/tests/test_load_persisted_stats.py new file mode 100644 index 00000000..76edbf51 --- /dev/null +++ b/router/tests/test_load_persisted_stats.py @@ -0,0 +1,61 @@ +import json +import pytest +from unittest.mock import patch, mock_open + +import router.main +from router.main import load_persisted_stats + +@pytest.fixture +def mock_stats(): + # Setup a clean stats dictionary for testing + clean_stats = { + "total_requests": 0, + "nested_dict": {"a": 1, "b": 2}, + "existing_key": "value" + } + with patch.dict(router.main.stats, clean_stats, clear=True): + yield router.main.stats + +def test_load_persisted_stats_file_not_exists(mock_stats): + with patch("router.main.os.path.exists", return_value=False) as mock_exists: + load_persisted_stats() + mock_exists.assert_called_once_with(router.main.STATS_JSON_PATH) + # Stats should remain unchanged + assert mock_stats["total_requests"] == 0 + +def test_load_persisted_stats_success(mock_stats): + mock_data = { + "total_requests": 100, + "nested_dict": {"b": 3, "c": 4}, + "new_key": "new_value" + } + mock_json = json.dumps(mock_data) + + with patch("router.main.os.path.exists", return_value=True): + with patch("router.main.open", mock_open(read_data=mock_json)): + with patch("router.main.logger.info") as mock_logger: + load_persisted_stats() + + # Assert simple value updated via else block + assert mock_stats["total_requests"] == 100 + # Assert nested_dict updated via if block (b updated, c added, a unchanged) + assert mock_stats["nested_dict"] == {"a": 1, "b": 3, "c": 4} + # Assert new_key added via else block + assert mock_stats["new_key"] == "new_value" + # Assert existing_key unchanged + assert mock_stats["existing_key"] == "value" + + mock_logger.assert_called_once_with("✓ Successfully loaded persisted gateway statistics from disk.") + +def test_load_persisted_stats_exception(mock_stats): + with patch("router.main.os.path.exists", return_value=True): + with patch("router.main.open", side_effect=Exception("Mock read error")): + with patch("router.main.logger.error") as mock_logger: + load_persisted_stats() + + # Stats should remain unchanged + assert mock_stats["total_requests"] == 0 + + # Error should be logged + mock_logger.assert_called_once() + assert "Failed to load persisted stats: Mock read error" in mock_logger.call_args[0][0] diff --git a/test_memory_mcp.py b/router/tests/test_memory_mcp.py similarity index 100% rename from test_memory_mcp.py rename to router/tests/test_memory_mcp.py diff --git a/scripts/benchmark_tokens.py b/scripts/benchmark_tokens.py new file mode 100644 index 00000000..d701b4eb --- /dev/null +++ b/scripts/benchmark_tokens.py @@ -0,0 +1,76 @@ +import sys +import os +from pathlib import Path + +# Set CONFIG_PATH and ROUTER_API_KEY for import +os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "router" / "config.yaml") +os.environ["ROUTER_API_KEY"] = "local-token" +# Add the parent directory and the router directory to the path +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "router")) + +from router.main import estimate_prompt_tokens + +def verify_accuracy(): + """Benchmarking utility to verify token estimation accuracy across content types.""" + # Test cases inspired by the problem description + test_cases = [ + { + "name": "English prose", + "content": "This is a standard English prose sentence intended to evaluate the accuracy of the token estimation heuristic for typical content. " * 5, + "actual_tokens": 110, + }, + { + "name": "Python code", + "content": """ +def calculate_factorial(n): + if n == 0: + return 1 + else: + return n * calculate_factorial(n-1) + +for i in range(10): + print(f"Factorial of {i} is {calculate_factorial(i)}") +""" * 3, + "actual_tokens": 150, + }, + { + "name": "CJK text", + "content": "这是一个测试,用于验证中文字符的令牌估算逻辑。它应该比字符计数更准确。" * 5, + "actual_tokens": 60, + }, + { + "name": "Whitespace-padded JSON", + "content": '{\n "key": "value",\n "nested": {\n "inner": "data"\n }\n}\n' * 5, + "actual_tokens": 60, + }, + { + "name": "Emoji", + "content": "🚀🔥-🤖✨-📈💎-🚨🛠️-🌐" * 5, + "actual_tokens": 25, + } + ] + + print(f"{'Case':<25} | {'Actual':<7} | {'Estimated':<9} | {'Error':<7}") + print("-" * 55) + + all_passed = True + for case in test_cases: + body = {"messages": [{"content": case["content"]}]} + est = estimate_prompt_tokens(body) - 50 # Subtract metadata overhead + error = abs(est - case["actual_tokens"]) / case["actual_tokens"] + print(f"{case['name']:<25} | {case['actual_tokens']:<7} | {est:<9} | {error:.1%}") + # Acceptance criteria: within ±25% for these rough heuristics + if error > 0.25: + print(f" --> FAILURE: {case['name']} error exceeds target threshold") + all_passed = False + + assert all_passed, "Token estimation accuracy benchmark failed" + +if __name__ == "__main__": + try: + verify_accuracy() + sys.exit(0) + except AssertionError as e: + print(f"\nERROR: {e}") + sys.exit(1) diff --git a/scripts/get_pr_status.py b/scripts/get_pr_status.py new file mode 100644 index 00000000..d65f65f1 --- /dev/null +++ b/scripts/get_pr_status.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import subprocess +import json +import sys +from typing import Sequence + + +def run_cmd(argv: Sequence[str]) -> str: + """Runs a command and returns stripped stdout.""" + result = subprocess.run(argv, shell=False, capture_output=True, text=True, check=True, timeout=30) + return result.stdout.strip() + + +def get_pr_status(pr_id: str = "") -> None: + """Fetches and prints the status of a PR using gh CLI.""" + cmd = ["gh", "pr", "view"] + if pr_id: + cmd.append(pr_id) + cmd.extend(["--json", "state,reviewDecision,statusCheckRollup"]) + + try: + output = run_cmd(cmd) + data = json.loads(output) + + state = data.get("state") + review = data.get("reviewDecision") or "NONE" + checks = data.get("statusCheckRollup", []) + + # Summarize checks + success_count = 0 + total_count = len(checks) + for check in checks: + # gh CLI returns conclusion for CheckRun and state for StatusContext + conclusion = check.get("conclusion") or check.get("state") + if conclusion == "SUCCESS": + success_count += 1 + + print(f"PR Status: {state}") + print(f"Review Decision: {review}") + print(f"Checks: {success_count}/{total_count} passed") + + except subprocess.CalledProcessError as e: + print(f"Error: Failed to fetch PR status: {e.stderr.strip()}", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError: + print("Error: Failed to parse gh CLI output", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"An unexpected error occurred: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + pr_id = sys.argv[1] if len(sys.argv) > 1 else "" + get_pr_status(pr_id) + + +if __name__ == "__main__": + main() diff --git a/host_agy_daemon.py b/scripts/host_agy_daemon.py similarity index 100% rename from host_agy_daemon.py rename to scripts/host_agy_daemon.py diff --git a/sync_gemini_token.py b/scripts/sync_gemini_token.py similarity index 100% rename from sync_gemini_token.py rename to scripts/sync_gemini_token.py diff --git a/test_quota_reset.sh b/scripts/test_quota_reset.sh similarity index 100% rename from test_quota_reset.sh rename to scripts/test_quota_reset.sh diff --git a/verify_breaker.py b/scripts/verification/verify_breaker.py similarity index 100% rename from verify_breaker.py rename to scripts/verification/verify_breaker.py diff --git a/watch_quota.sh b/scripts/watch_quota.sh similarity index 100% rename from watch_quota.sh rename to scripts/watch_quota.sh diff --git a/start-stack.sh b/start-stack.sh index c9b826dc..03bb1220 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -31,12 +31,6 @@ if [ -f "$ENV_FILE" ]; then fi # Ensure openssl is installed if we need to generate passwords/keys -if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ]; then - if ! command -v openssl &>/dev/null; then - echo "❌ Error: 'openssl' is required to generate secure random keys but was not found in PATH." - exit 1 - fi -fi if [ -z "$OPENROUTER_API_KEY" ]; then @@ -101,7 +95,7 @@ else echo "⚠️ Warning: Host agy daemon not responding on port 5005" fi -if [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ]; then +if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ] || [ -z "$MINIO_ROOT_USER" ] || [ -z "$MINIO_ROOT_PASSWORD" ]; then if ! command -v openssl &>/dev/null; then echo "❌ Error: 'openssl' is required to generate secure random keys but was not found in PATH." exit 1 @@ -141,12 +135,43 @@ if [ -z "$LITELLM_MASTER_KEY" ]; then exit 1 fi +if [ -z "$LANGFUSE_INIT_USER_PASSWORD" ]; then + LANGFUSE_INIT_USER_PASSWORD="$(openssl rand -hex 16)" + echo "LANGFUSE_INIT_USER_PASSWORD=\"$LANGFUSE_INIT_USER_PASSWORD\"" >> "$ENV_FILE" + echo "✓ Generated new LANGFUSE_INIT_USER_PASSWORD and saved to $ENV_FILE" +fi + +if [ -z "$REDIS_AUTH" ]; then + REDIS_AUTH="$(openssl rand -hex 16)" + echo "REDIS_AUTH=\"$REDIS_AUTH\"" >> "$ENV_FILE" + echo "✓ Generated new REDIS_AUTH and saved to $ENV_FILE" +fi + +if [ -z "$CLICKHOUSE_PASSWORD" ]; then + CLICKHOUSE_PASSWORD="$(openssl rand -hex 16)" + echo "CLICKHOUSE_PASSWORD=\"$CLICKHOUSE_PASSWORD\"" >> "$ENV_FILE" + echo "✓ Generated new CLICKHOUSE_PASSWORD and saved to $ENV_FILE" +fi + if [ -z "$ROUTER_API_KEY" ]; then ROUTER_API_KEY="$(openssl rand -hex 32)" echo "ROUTER_API_KEY=\"$ROUTER_API_KEY\"" >> "$ENV_FILE" echo "✓ Generated new ROUTER_API_KEY and saved to $ENV_FILE" fi +if [ -z "$MINIO_ROOT_USER" ]; then + MINIO_ROOT_USER="minio-$(openssl rand -hex 4)" + echo "MINIO_ROOT_USER=\"$MINIO_ROOT_USER\"" >> "$ENV_FILE" + echo "✓ Generated new MINIO_ROOT_USER and saved to $ENV_FILE" +fi + +if [ -z "$MINIO_ROOT_PASSWORD" ]; then + MINIO_ROOT_PASSWORD="$(openssl rand -hex 16)" + echo "MINIO_ROOT_PASSWORD=\"$MINIO_ROOT_PASSWORD\"" >> "$ENV_FILE" + echo "✓ Generated new MINIO_ROOT_PASSWORD and saved to $ENV_FILE" +fi + + # DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER in router config is resolved at runtime from env @@ -255,7 +280,7 @@ setup_minio_buckets() { # Ensure mc alias points to the correct MinIO S3 API port (9002, not 9000) # The default 'local' alias in the MinIO image points to :9000 which is ClickHouse, # not MinIO. We must override it. - podman exec agent-router-pod-minio-s3 mc alias set local http://127.0.0.1:9002 minioadmin minioadmin 2>/dev/null + podman exec agent-router-pod-minio-s3 mc alias set local http://127.0.0.1:9002 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" 2>/dev/null # Create required buckets (idempotent) local BUCKETS=("langfuse-events" "proj-triage-gateway-id") @@ -356,7 +381,7 @@ if podman pod exists agent-router-pod 2>/dev/null; then fi render_pod_yaml() { - export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY + export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD python3 - "$WORKDIR/pod.yaml" <<'PY' import os, sys, urllib.parse uid = os.getuid() @@ -371,11 +396,14 @@ placeholders = [ "NEXTAUTH_SECRET_PLACEHOLDER", "SALT_PLACEHOLDER", "ENCRYPTION_KEY_PLACEHOLDER", - "postgres-password-***" + "postgres-password-***", + "MINIO_USER_PLACEHOLDER", + "MINIO_PASSWORD_PLACEHOLDER" + "LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER" ] for ph in placeholders: if ph not in text: - sys.stderr.write(f"Error: Required placeholder '{ph}' not found in pod.yaml\n") + sys.stderr.write(f"Error: Required placeholder '{ph}' not found in pod.yaml. Ensure you are using the latest version of the template.\n") sys.exit(1) text = text.replace("/home/gpav/Vrac/LAB/AI/LLM-Routing", os.environ["WORKDIR"]) text = text.replace("/home/gpav/", os.environ["HOME"] + "/") @@ -388,6 +416,9 @@ text = text.replace("postgres-password-***", os.environ["POSTGRES_PASSWORD"]) text = text.replace("NEXTAUTH_SECRET_PLACEHOLDER", os.environ["NEXTAUTH_SECRET"]) text = text.replace("SALT_PLACEHOLDER", os.environ["SALT"]) text = text.replace("ENCRYPTION_KEY_PLACEHOLDER", os.environ["ENCRYPTION_KEY"]) +text = text.replace("MINIO_USER_PLACEHOLDER", os.environ["MINIO_ROOT_USER"]) +text = text.replace("MINIO_PASSWORD_PLACEHOLDER", os.environ["MINIO_ROOT_PASSWORD"]) +text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", os.environ["LANGFUSE_INIT_USER_PASSWORD"]) sys.stdout.write(text) PY } diff --git a/test_a2_verify.py b/tests/test_a2_verify.py similarity index 100% rename from test_a2_verify.py rename to tests/test_a2_verify.py diff --git a/test_agy_behavior.py b/tests/test_agy_behavior.py similarity index 100% rename from test_agy_behavior.py rename to tests/test_agy_behavior.py diff --git a/test_agy_tiers.py b/tests/test_agy_tiers.py similarity index 100% rename from test_agy_tiers.py rename to tests/test_agy_tiers.py diff --git a/test_antigravity.py b/tests/test_antigravity.py similarity index 100% rename from test_antigravity.py rename to tests/test_antigravity.py diff --git a/test_atomic_write.py b/tests/test_atomic_write.py similarity index 100% rename from test_atomic_write.py rename to tests/test_atomic_write.py diff --git a/test_check_http_endpoint.py b/tests/test_check_http_endpoint.py similarity index 100% rename from test_check_http_endpoint.py rename to tests/test_check_http_endpoint.py diff --git a/test_circuit_breaker.py b/tests/test_circuit_breaker.py similarity index 100% rename from test_circuit_breaker.py rename to tests/test_circuit_breaker.py diff --git a/test_classifier_accuracy.py b/tests/test_classifier_accuracy.py similarity index 100% rename from test_classifier_accuracy.py rename to tests/test_classifier_accuracy.py diff --git a/test_compute_free_model_score.py b/tests/test_compute_free_model_score.py similarity index 100% rename from test_compute_free_model_score.py rename to tests/test_compute_free_model_score.py diff --git a/tests/test_get_pr_status.py b/tests/test_get_pr_status.py new file mode 100644 index 00000000..93885a14 --- /dev/null +++ b/tests/test_get_pr_status.py @@ -0,0 +1,84 @@ +import pytest +import subprocess +import json +from unittest.mock import patch, MagicMock +from scripts.get_pr_status import run_cmd, get_pr_status + +def test_run_cmd_success(): + output = run_cmd(["echo", "hello"]) + assert output == "hello" + +def test_run_cmd_strips_whitespace(): + output = run_cmd(["echo", " hello "]) + assert output == "hello" + +def test_run_cmd_error(): + with pytest.raises(subprocess.CalledProcessError): + run_cmd(["false"]) + +@patch("scripts.get_pr_status.subprocess.run") +def test_run_cmd_timeout(mock_run): + # run_cmd has a 30s timeout. We mock subprocess.run to raise it immediately. + mock_run.side_effect = subprocess.TimeoutExpired(["sleep", "0.1"], 30) + with pytest.raises(subprocess.TimeoutExpired): + run_cmd(["sleep", "0.1"]) + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_success(mock_run_cmd, capsys): + mock_data = { + "state": "OPEN", + "reviewDecision": "APPROVED", + "statusCheckRollup": [ + {"conclusion": "SUCCESS", "name": "test1"}, + {"state": "SUCCESS", "name": "test2"}, + {"conclusion": "FAILURE", "name": "test3"} + ] + } + mock_run_cmd.return_value = json.dumps(mock_data) + + get_pr_status("123") + + captured = capsys.readouterr() + assert "PR Status: OPEN" in captured.out + assert "Review Decision: APPROVED" in captured.out + assert "Checks: 2/3 passed" in captured.out + mock_run_cmd.assert_called_once_with(["gh", "pr", "view", "123", "--json", "state,reviewDecision,statusCheckRollup"]) + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_no_id(mock_run_cmd, capsys): + mock_data = { + "state": "MERGED", + "reviewDecision": None, + "statusCheckRollup": [] + } + mock_run_cmd.return_value = json.dumps(mock_data) + + get_pr_status() + + captured = capsys.readouterr() + assert "PR Status: MERGED" in captured.out + assert "Review Decision: NONE" in captured.out + assert "Checks: 0/0 passed" in captured.out + mock_run_cmd.assert_called_once_with(["gh", "pr", "view", "--json", "state,reviewDecision,statusCheckRollup"]) + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_error(mock_run_cmd, capsys): + mock_run_cmd.side_effect = subprocess.CalledProcessError(1, ["gh"], stderr="gh not found") + + with pytest.raises(SystemExit) as e: + get_pr_status("123") + + assert e.value.code == 1 + captured = capsys.readouterr() + assert "Error: Failed to fetch PR status: gh not found" in captured.err + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_invalid_json(mock_run_cmd, capsys): + mock_run_cmd.return_value = "invalid json" + + with pytest.raises(SystemExit) as e: + get_pr_status("123") + + assert e.value.code == 1 + captured = capsys.readouterr() + assert "Error: Failed to parse gh CLI output" in captured.err diff --git a/tests/test_host_agy_daemon.py b/tests/test_host_agy_daemon.py new file mode 100644 index 00000000..61f84e86 --- /dev/null +++ b/tests/test_host_agy_daemon.py @@ -0,0 +1,490 @@ +import asyncio +import json +import os +import socket +import threading +import urllib.error +import urllib.request +from unittest.mock import AsyncMock + +import pytest + +import sys +from pathlib import Path + +# Dynamic project root discovery +root = Path(__file__).resolve() +while root.parent != root and not (root / ".git").exists(): + root = root.parent +sys.path.insert(0, str(root)) +sys.path.insert(0, str(root / "scripts")) + +import host_agy_daemon + +def find_free_port(): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + return s.getsockname()[1] + +@pytest.fixture +def daemon_server(): + port = find_free_port() + host_agy_daemon.PORT = port + + server = host_agy_daemon.ThreadingHTTPServer(('127.0.0.1', port), host_agy_daemon.AgyDaemonHandler) + server_thread = threading.Thread(target=server.serve_forever, daemon=True) + server_thread.start() + + yield f"http://127.0.0.1:{port}" + + server.shutdown() + server.server_close() + server_thread.join() + +def test_get_last_conversation_id(monkeypatch, tmp_path): + cache_file = tmp_path / "last_conversations.json" + cache_file.write_text(json.dumps({"/fake/cwd": "conv_123"})) + + monkeypatch.setattr(host_agy_daemon, "CACHE_FILE", str(cache_file)) + monkeypatch.setattr(host_agy_daemon.os, "getcwd", lambda: "/fake/cwd") + + assert host_agy_daemon.get_last_conversation_id() == "conv_123" + + monkeypatch.setattr(host_agy_daemon.os, "getcwd", lambda: "/other/cwd") + assert host_agy_daemon.get_last_conversation_id() is None + +def test_get_last_conversation_id_no_file(monkeypatch): + monkeypatch.setattr(host_agy_daemon, "CACHE_FILE", "/does/not/exist.json") + assert host_agy_daemon.get_last_conversation_id() is None + +def test_get_last_conversation_id_invalid_json(monkeypatch, tmp_path): + cache_file = tmp_path / "last_conversations.json" + cache_file.write_text("invalid json") + + monkeypatch.setattr(host_agy_daemon, "CACHE_FILE", str(cache_file)) + assert host_agy_daemon.get_last_conversation_id() is None + +def test_get_last_conversation_id_io_error(monkeypatch): + monkeypatch.setattr(host_agy_daemon, "CACHE_FILE", "/fake/cache.json") + monkeypatch.setattr(host_agy_daemon.os.path, "exists", lambda x: True) + def mock_open_err(*args, **kwargs): + raise IOError("permission denied") + monkeypatch.setattr("builtins.open", mock_open_err) + assert host_agy_daemon.get_last_conversation_id() is None + +def test_daemon_post_404(daemon_server): + req = urllib.request.Request(f"{daemon_server}/invalid", method="POST") + with pytest.raises(urllib.error.HTTPError) as exc: + urllib.request.urlopen(req) + assert exc.value.code == 404 + +def test_daemon_post_stream_false(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": False, "conversation_id": "conv_abc", "model_override": "gpt-4"}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + assert args == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_abc", "--print", "test prompt") + assert kwargs.get("env", {}).get("CASCADE_DEFAULT_MODEL_OVERRIDE") == "gpt-4" + mock_proc = AsyncMock() + mock_proc.returncode = 0 + mock_proc.wait = AsyncMock() + + if "stdout" in kwargs: + with open(kwargs["stdout"].name, "w") as f: + f.write("mocked stdout output") + if "stderr" in kwargs: + with open(kwargs["stderr"].name, "w") as f: + f.write("mocked stderr output") + + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + monkeypatch.setattr(host_agy_daemon, "get_last_conversation_id", lambda: "last_conv_456") + + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + + assert data["returncode"] == 0 + assert data["stdout"] == "mocked stdout output" + assert data["stderr"] == "mocked stderr output" + assert data["conversation_id"] == "last_conv_456" + +def test_daemon_post_stream_false_timeout(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": False, "timeout": 0.1}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + # Make wait take longer than timeout + async def slow_wait(): + await asyncio.sleep(0.5) + mock_proc.wait = slow_wait + # Make kill synchronous + mock_proc.kill = lambda: None + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + + assert data["returncode"] == -1 + assert data["stderr"] == "TIMEOUT" + +def test_daemon_post_stream_true(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": True, "model_override": "test-model"}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + assert args == (host_agy_daemon.AGY_BINARY, "--print", "test prompt") + assert kwargs.get("env", {}).get("CASCADE_DEFAULT_MODEL_OVERRIDE") == "test-model" + mock_proc = AsyncMock() + mock_proc.returncode = 0 + mock_proc.wait = AsyncMock() + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + monkeypatch.setattr(host_agy_daemon, "get_last_conversation_id", lambda: "last_conv_456") + + read_calls = 0 + def mock_read(fd, n): + nonlocal read_calls + if read_calls == 0: + read_calls += 1 + return b"token1\r\n" + elif read_calls == 1: + read_calls += 1 + return b"token2\r\n" + return b"" + + monkeypatch.setattr(host_agy_daemon.os, "read", mock_read) + + with urllib.request.urlopen(req) as resp: + content = resp.read().decode().strip() + lines = content.split("\n") + + assert len(lines) == 3 + assert json.loads(lines[0]) == {"type": "token", "content": "token1\n"} + assert json.loads(lines[1]) == {"type": "token", "content": "token2\n"} + assert json.loads(lines[2]) == {"type": "status", "returncode": 0, "conversation_id": "last_conv_456"} + +def test_daemon_post_stream_true_exec_error(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": True}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + raise Exception("exec failed") + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + + with urllib.request.urlopen(req) as resp: + content = resp.read().decode().strip() + lines = content.split("\n") + + assert len(lines) == 1 + assert json.loads(lines[0]) == {"type": "status", "returncode": -1, "stderr": "exec failed"} + +def test_daemon_post_stream_true_timeout(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": True, "timeout": 0.1}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + async def slow_wait(): + await asyncio.sleep(0.5) + mock_proc.wait = slow_wait + # Make kill synchronous + mock_proc.kill = lambda: None + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + monkeypatch.setattr(host_agy_daemon, "get_last_conversation_id", lambda: None) + + read_calls = 0 + def mock_read(fd, n): + nonlocal read_calls + if read_calls == 0: + read_calls += 1 + return b"token1\n" + return b"" + + monkeypatch.setattr(host_agy_daemon.os, "read", mock_read) + + with urllib.request.urlopen(req) as resp: + content = resp.read().decode().strip() + lines = content.split("\n") + + assert len(lines) == 2 + assert json.loads(lines[0]) == {"type": "token", "content": "token1\n"} + assert json.loads(lines[1]) == {"type": "status", "returncode": -1, "conversation_id": None} + +def test_log_message_silenced(): + # Instantiate the class, bypassing BaseHTTPRequestHandler.__init__ + handler = host_agy_daemon.AgyDaemonHandler.__new__(host_agy_daemon.AgyDaemonHandler) + # Shouldn't raise any error + handler.log_message("format %s", "arg") + +def test_run_server_interrupt(monkeypatch): + # Mock serve_forever to raise KeyboardInterrupt + def mock_serve_forever(self): + raise KeyboardInterrupt() + + monkeypatch.setattr(host_agy_daemon.ThreadingHTTPServer, "serve_forever", mock_serve_forever) + + # Track if server_close was called + close_called = False + def mock_server_close(self): + nonlocal close_called + close_called = True + + monkeypatch.setattr(host_agy_daemon.ThreadingHTTPServer, "server_close", mock_server_close) + + # Should not raise exception + host_agy_daemon.run_server() + assert close_called + +def test_daemon_post_stream_false_no_model_override(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + assert "CASCADE_DEFAULT_MODEL_OVERRIDE" not in kwargs.get("env", {}) + mock_proc = AsyncMock() + mock_proc.returncode = 0 + mock_proc.wait = AsyncMock() + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + monkeypatch.setattr(host_agy_daemon.os.environ, "copy", lambda: {"CASCADE_DEFAULT_MODEL_OVERRIDE": "old-model"}) + + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + + assert data["returncode"] == 0 + +def test_daemon_post_stream_true_read_oserror(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": True}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + mock_proc.wait = AsyncMock() + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + + def mock_read(fd, n): + raise OSError("read error") + + monkeypatch.setattr(host_agy_daemon.os, "read", mock_read) + + with urllib.request.urlopen(req) as resp: + content = resp.read().decode().strip() + lines = content.split("\n") + + assert len(lines) == 1 + assert json.loads(lines[0])["type"] == "status" + +def test_daemon_post_stream_true_timeout_kill_fail(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": True, "timeout": 0.1}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + async def slow_wait(): + await asyncio.sleep(0.5) + mock_proc.wait = slow_wait + def mock_kill(): + raise Exception("kill failed") + mock_proc.kill = mock_kill + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + monkeypatch.setattr(host_agy_daemon.os, "read", lambda fd, n: b"") + monkeypatch.setattr(host_agy_daemon, "get_last_conversation_id", lambda: None) + + with urllib.request.urlopen(req) as resp: + content = resp.read().decode().strip() + lines = content.split("\n") + + assert len(lines) == 1 + assert json.loads(lines[0]) == {"type": "status", "returncode": -1, "conversation_id": None} + +def test_daemon_post_stream_true_wait_exception(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": True}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + async def mock_wait(): + raise Exception("wait failed") + mock_proc.wait = mock_wait + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + monkeypatch.setattr(host_agy_daemon.os, "read", lambda fd, n: b"") + monkeypatch.setattr(host_agy_daemon, "get_last_conversation_id", lambda: None) + + with urllib.request.urlopen(req) as resp: + content = resp.read().decode().strip() + lines = content.split("\n") + + assert len(lines) == 1 + assert json.loads(lines[0]) == {"type": "status", "returncode": -1, "conversation_id": None} + +def test_daemon_post_stream_false_timeout_kill_fail(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": False, "timeout": 0.1}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + async def slow_wait(): + await asyncio.sleep(0.5) + mock_proc.wait = slow_wait + def mock_kill(): + raise Exception("kill failed") + mock_proc.kill = mock_kill + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + + assert data["returncode"] == -1 + assert data["stderr"] == "TIMEOUT" + +def test_daemon_post_stream_false_wait_exception(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + async def mock_wait(): + raise Exception("wait failed") + mock_proc.wait = mock_wait + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + + assert data["returncode"] == -1 + +def test_daemon_post_stream_false_file_read_error(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + mock_proc.wait = AsyncMock() + + # Corrupt the temp files to cause read exceptions + os.unlink(kwargs["stdout"].name) + os.unlink(kwargs["stderr"].name) + + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + + assert data["returncode"] == 0 + assert data["stdout"] == "" + assert data["stderr"] == "" + +def test_daemon_post_stream_false_unlink_error(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + mock_proc = AsyncMock() + mock_proc.returncode = 0 + mock_proc.wait = AsyncMock() + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + + def mock_unlink(path): + raise Exception("unlink failed") + + monkeypatch.setattr(host_agy_daemon.os, "unlink", mock_unlink) + + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + + assert data["returncode"] == 0 + +def test_daemon_post_stream_true_with_conversation(daemon_server, monkeypatch): + req = urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps({"prompt": "test prompt", "stream": True, "conversation_id": "conv_789"}).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + + async def mock_exec(*args, **kwargs): + assert args == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_789", "--print", "test prompt") + mock_proc = AsyncMock() + mock_proc.returncode = 0 + mock_proc.wait = AsyncMock() + return mock_proc + + monkeypatch.setattr(host_agy_daemon.asyncio, "create_subprocess_exec", mock_exec) + monkeypatch.setattr(host_agy_daemon, "get_last_conversation_id", lambda: "conv_789") + monkeypatch.setattr(host_agy_daemon.os, "read", lambda fd, n: b"") + + with urllib.request.urlopen(req) as resp: + content = resp.read().decode().strip() + lines = content.split("\n") + + assert len(lines) == 1 + assert json.loads(lines[0]) == {"type": "status", "returncode": 0, "conversation_id": "conv_789"} diff --git a/test_map_tool_to_category.py b/tests/test_map_tool_to_category.py similarity index 100% rename from test_map_tool_to_category.py rename to tests/test_map_tool_to_category.py diff --git a/test_models_proxy.py b/tests/test_models_proxy.py similarity index 100% rename from test_models_proxy.py rename to tests/test_models_proxy.py diff --git a/test_pie_chart_gradient.py b/tests/test_pie_chart_gradient.py similarity index 100% rename from test_pie_chart_gradient.py rename to tests/test_pie_chart_gradient.py diff --git a/tests/test_read_annotations_async.py b/tests/test_read_annotations_async.py new file mode 100644 index 00000000..b401bc8b --- /dev/null +++ b/tests/test_read_annotations_async.py @@ -0,0 +1,128 @@ +import pytest +from unittest.mock import patch, AsyncMock, MagicMock +import copy +import sys +import os +import json +import asyncio + +# Ensure the root directory is in the path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +import router.main +from router.main import _read_annotations_async + +@pytest.fixture(autouse=True) +def clear_cache(): + router.main._annotations_cache.clear() + yield + +@pytest.mark.asyncio +async def test_read_annotations_async_initial_read(): + fake_path = "/tmp/annotations.json" + fake_data = {"annotation1": "data1"} + + # Mock aiofiles.open + mock_file = AsyncMock() + mock_file.read.return_value = '{"annotation1": "data1"}' + + mock_context_manager = MagicMock() + mock_context_manager.__aenter__ = AsyncMock(return_value=mock_file) + mock_context_manager.__aexit__ = AsyncMock(return_value=False) + + mock_aiofiles_open = MagicMock(return_value=mock_context_manager) + + with patch("os.path.getmtime", return_value=100.0) as mock_getmtime, \ + patch("aiofiles.open", mock_aiofiles_open) as mock_open: + + result = await _read_annotations_async(fake_path) + + mock_getmtime.assert_called_once_with(fake_path) + mock_open.assert_called_once_with(fake_path, "r", encoding="utf-8") + assert result == fake_data + + # Verify cache is populated + assert fake_path in router.main._annotations_cache + assert router.main._annotations_cache[fake_path]["mtime"] == 100.0 + assert router.main._annotations_cache[fake_path]["data"] == fake_data + +@pytest.mark.asyncio +async def test_read_annotations_async_cache_hit(): + fake_path = "/tmp/annotations.json" + fake_data = {"annotation1": "data1"} + + # Pre-populate cache + router.main._annotations_cache[fake_path] = {"mtime": 100.0, "data": fake_data} + + # Mock aiofiles.open (should NOT be called) + mock_aiofiles_open = MagicMock() + + with patch("os.path.getmtime", return_value=100.0) as mock_getmtime, \ + patch("aiofiles.open", mock_aiofiles_open) as mock_open: + + result = await _read_annotations_async(fake_path) + + mock_getmtime.assert_called_once_with(fake_path) + mock_open.assert_not_called() + assert result == fake_data + +@pytest.mark.asyncio +async def test_read_annotations_async_cache_invalidation(): + fake_path = "/tmp/annotations.json" + fake_data_old = {"annotation1": "data1"} + fake_data_new = {"annotation2": "data2"} + + # Pre-populate cache with old mtime + router.main._annotations_cache[fake_path] = {"mtime": 100.0, "data": fake_data_old} + + mock_file = AsyncMock() + mock_file.read.return_value = '{"annotation2": "data2"}' + + mock_context_manager = MagicMock() + mock_context_manager.__aenter__ = AsyncMock(return_value=mock_file) + mock_context_manager.__aexit__ = AsyncMock(return_value=False) + + mock_aiofiles_open = MagicMock(return_value=mock_context_manager) + + with patch("os.path.getmtime", return_value=200.0) as mock_getmtime, \ + patch("aiofiles.open", mock_aiofiles_open) as mock_open: + + result = await _read_annotations_async(fake_path) + + mock_getmtime.assert_called_once_with(fake_path) + mock_open.assert_called_once_with(fake_path, "r", encoding="utf-8") + assert result == fake_data_new + + # Verify cache is updated + assert router.main._annotations_cache[fake_path]["mtime"] == 200.0 + assert router.main._annotations_cache[fake_path]["data"] == fake_data_new + +@pytest.mark.asyncio +async def test_read_annotations_async_deepcopy(): + fake_path = "/tmp/annotations.json" + fake_data = {"annotation1": {"nested": "value"}} + + # Pre-populate cache + router.main._annotations_cache[fake_path] = {"mtime": 100.0, "data": fake_data} + + with patch("os.path.getmtime", return_value=100.0): + # First read + result1 = await _read_annotations_async(fake_path) + + # Mutate the result + result1["annotation1"]["nested"] = "mutated" + + # Second read + result2 = await _read_annotations_async(fake_path) + + # Verify second read returns original data, not mutated + assert result2["annotation1"]["nested"] == "value" + assert router.main._annotations_cache[fake_path]["data"]["annotation1"]["nested"] == "value" + +@pytest.mark.asyncio +async def test_read_annotations_async_file_not_found(): + fake_path = "/tmp/annotations.json" + + with patch("os.path.getmtime", side_effect=FileNotFoundError): + with pytest.raises(FileNotFoundError): + await _read_annotations_async(fake_path) diff --git a/test_record_tool_usage.py b/tests/test_record_tool_usage.py similarity index 100% rename from test_record_tool_usage.py rename to tests/test_record_tool_usage.py diff --git a/test_src_badge.py b/tests/test_src_badge.py similarity index 100% rename from test_src_badge.py rename to tests/test_src_badge.py diff --git a/test_stream_latency.py b/tests/test_stream_latency.py similarity index 100% rename from test_stream_latency.py rename to tests/test_stream_latency.py diff --git a/test_sync_gemini_token.py b/tests/test_sync_gemini_token.py similarity index 100% rename from test_sync_gemini_token.py rename to tests/test_sync_gemini_token.py From d2391664fa32186024dfe840e7e249e240cde013 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:17:29 +0200 Subject: [PATCH 02/22] docs: update LiteLLM version pinning and test suite paths --- README.md | 4 ++-- scripts/README.md | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c093e9fe..7c80825c 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ graph TD style QwenLocal fill:#f0f0f0,stroke:#999,stroke-width:1px; ``` -> **Version Pin**: LiteLLM Gateway runs `ghcr.io/berriai/litellm:v1.88.0`. See §3B for pinning policy. +> **Version Pin**: LiteLLM Gateway runs `ghcr.io/berriai/litellm:v1.90.2`. See §3B for pinning policy. --- @@ -269,7 +269,7 @@ Exposes the entry endpoint (`http://localhost:5000/v1`) and evaluates prompt com > Model capabilities, token limits, and costs are visible in LiteLLM's Model Hub Table at `http://localhost:4000/ui/?page=model-hub-table` (or port 4000 on the gateway host). ### B. LiteLLM Proxy Gateway (`litellm/config.yaml`) -- **Version Pinning**: The LiteLLM gateway runs `ghcr.io/berriai/litellm:v1.88.0` (latest stable as of June 2026). The tag is explicitly pinned in `pod.yaml` — never use `:latest`. Check available tags with `skopeo list-tags docker://ghcr.io/berriai/litellm` before upgrading. ClickHouse runs `docker.io/clickhouse/clickhouse-server:26.5.1` (upgraded from 24.8, June 2026). Valkey Cache runs `docker.io/valkey/valkey:9.1.0-alpine` (upgraded from 8, June 2026). +- **Version Pinning**: The LiteLLM gateway runs `ghcr.io/berriai/litellm:v1.90.2` (latest stable as of June 2026). The tag is explicitly pinned in `pod.yaml` — never use `:latest`. Check available tags with `skopeo list-tags docker://ghcr.io/berriai/litellm` before upgrading. ClickHouse runs `docker.io/clickhouse/clickhouse-server:26.5.1` (upgraded from 24.8, June 2026). Valkey Cache runs `docker.io/valkey/valkey:9.1.0-alpine` (upgraded from 8, June 2026). Orchestrates routing fallback chains, Redis caching, and telemetry callbacks: - **`drop_params: true`**: Automatically strips unsupported arguments when transitioning to models that don't support them. - **Request Timeouts (`300s`)**: Provides ample padding to prevent connection aborts during dynamic RAM swapping operations on the local GPU `llama-server`. diff --git a/scripts/README.md b/scripts/README.md index 0139e8d3..4086a932 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -60,26 +60,26 @@ These tools are used to benchmark the prompt classifier and extract datasets fro --- -## 4. Integration Test Suite (Root Directory) +## 4. Integration Test Suite -The integration test suite is located in the root directory. Tests are categorized below based on their primary function: +The integration test suite is located in the `tests/` and `scripts/` directories. Tests are categorized below based on their primary function: ### Circuit Breaker Tests -- **`test_circuit_breaker.py`**: Unit/integration tests for the dual circuit breaker (`router/circuit_breaker.py`), covering independent Google/Vendor tiers and probe-granting logic. -- **`verify_breaker.py`**: Sanity verification check for the circuit breaker. -- **`test_a2_verify.py`**: Quick sanity integration check for the agy proxy circuit breaker. +- **`tests/test_circuit_breaker.py`**: Unit/integration tests for the dual circuit breaker (`router/circuit_breaker.py`), covering independent Google/Vendor tiers and probe-granting logic. +- **`scripts/verification/verify_breaker.py`**: Sanity verification check for the circuit breaker. +- **`tests/test_a2_verify.py`**: Quick sanity integration check for the agy proxy circuit breaker. ### Classifier Tests -- **`test_classifier_accuracy.py`**: Accuracy evaluation suite covering 25 system prompts. +- **`tests/test_classifier_accuracy.py`**: Accuracy evaluation suite covering 25 system prompts. ### Routing & Proxy Tests -- **`test_agy_tiers.py`**: Validates `agy` proxy model tier routing. -- **`test_antigravity.py`**: Tests the connection to the host Antigravity CLI daemon (`agentapi`). +- **`tests/test_agy_tiers.py`**: Validates `agy` proxy model tier routing. +- **`tests/test_antigravity.py`**: Tests the connection to the host Antigravity CLI daemon (`agentapi`). ### Performance & Monitoring Tests -- **`test_stream_latency.py`**: Measures Time-To-First-Token (TTFT) and token generation speed. +- **`tests/test_stream_latency.py`**: Measures Time-To-First-Token (TTFT) and token generation speed. ### Simulation Tests -- **`test_agy_behavior.py`**: Asserts the behavior of the `agy` CLI client under quota limits. -- **`test_quota_reset.sh`**: Simulates/triggers quota reset conditions. -- **`watch_quota.sh`**: Watch/polling script for observing quota status. +- **`tests/test_agy_behavior.py`**: Asserts the behavior of the `agy` CLI client under quota limits. +- **`scripts/test_quota_reset.sh`**: Simulates/triggers quota reset conditions. +- **`scripts/watch_quota.sh`**: Watch/polling script for observing quota status. From 4422f4605f4eb6fdae58ac8065f14407528e6ff3 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:22:00 +0200 Subject: [PATCH 03/22] chore: add bot rebase/conflict guidelines and regression report to .agents/ --- .agents/AGENTS.md | 8 +++ .agents/jules_regression_analysis.md | 81 ++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 .agents/jules_regression_analysis.md diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index c7de7c06..260dee2c 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -18,3 +18,11 @@ When working on this project, always refer to the dedicated **NotebookLM Compani Use the `notebooklm` MCP tools to search or ask questions about this codebase and stack: - Run `notebook_ask` with `notebook_id: "llm-triage-gateway"` to ground your reasoning or implementation plans. - If you need session continuation, remember to reuse the `session_id` returned by previous queries. + +## Git Rebase & Conflict Resolution Policy +To prevent directory reorganization regressions, outdated file restorations, or security credential overrides during merge conflict resolution, all automated agents must strictly follow these rules: + +1. **Rebase Over Merge**: Always fetch and rebase the topic/feature branch onto the latest `master` base branch (using `git rebase origin/master`) instead of performing `git merge`. +2. **Directory Rename Safety**: If Git reports conflicts related to moved directories or files, do not manually stage deletions of folders (`tests/`, `scripts/`) or re-create files at the root level. Direct all changes to the newly refactored paths. +3. **Verify Security Credentials**: Never accept resolutions that overwrite configuration files (`pod.yaml`, `start-stack.sh`) with hardcoded default passwords. Ensure placeholder-based configurations are preserved. +4. **Enforce Test Suite Count**: Run the full unit test suite (`pytest`) after conflict resolution. Verify that the total number of passing tests is equal to or greater than before the resolution. diff --git a/.agents/jules_regression_analysis.md b/.agents/jules_regression_analysis.md new file mode 100644 index 00000000..78899f39 --- /dev/null +++ b/.agents/jules_regression_analysis.md @@ -0,0 +1,81 @@ +# Analysis of Bot Merge Conflict Regressions & Guardrails + +This document breaks down the root causes of the regressions introduced by the automated agent (`jules[bot]`) during recent merges, compares Git resolution strategies, and outlines prompt modifications and guardrails to prevent future occurrences. + +--- + +## 1. What Went Wrong? + +The primary failure arose from a combination of **reorganization conflict logic** and **blind resolution choices**: + +### A. The Directory Rename / Delete Conflict Mismatch +* **Context**: PR #181 reorganized the repository by moving files at the root (like `test_a2_verify.py` and `host_agy_daemon.py`) into nested directories (`tests/` and `scripts/`). +* **The Bot's Branch State**: The bot was working on older branches (`perf-optimize-gemini-oauth-token`, `refactor-record-tool-usage`) whose base commits predated the reorganization. +* **The Failure**: When merging `master` into these older feature branches, Git detected conflict types like `rename/delete` or `directory rename detection`. Because the bot resolved conflicts locally inside the old workspace structure, it staged the **deletion** of the new files inside `tests/` and `scripts/` and re-introduced older, obsolete root-level files. +* **Result**: Merges from these branches back to `master` cleanly deleted the subfolder-bound tests and re-added old copies at the root. + +### B. Lack of Cross-Check Verification +* The bot resolved conflicts file-by-file without compiling the whole project or validating the full test suite (`pytest`) on the combined codebase. +* It accepted obsolete file versions wholesale (e.g., reverting the dynamic passwords in `pod.yaml` and `start-stack.sh` to hardcoded credentials) because it assumed conflict markers in one block did not impact security/logic blocks elsewhere. + +--- + +## 2. Which Git Strategy Should Have Been Used? + +Instead of merging `master` directly into older feature branches (which creates complex, multi-directional merge graphs), the following strategies are far safer for LLM agents: + +### Option A: `git rebase master` (Recommended for Feature Branches) +Rather than a merge commit, the agent should rebase the feature branch onto the latest `master` commit: +```bash +git checkout feature-branch +git fetch origin +git rebase origin/master +``` +* **Why it works**: Rebase replays each feature branch commit one-by-one on top of the reorganized `master` commit. +* **Rename Tracking**: Git's rename tracking algorithm handles moves seamlessly during a rebase. If a file was renamed from `test_a2_verify.py` to `tests/test_a2_verify.py` on `master`, Git will automatically apply the feature branch's modifications to `tests/test_a2_verify.py` instead of leaving them at the root. + +### Option B: Merge with explicit Merge Drivers +If rebasing is not used, the agent must inspect renames before committing: +```bash +git diff --name-status origin/master...HEAD +``` +This lists any deleted/added files to quickly verify that no folder moves were silently discarded. + +--- + +## 3. Recommended Prompt Guardrails and System Rules + +To prevent automated agents from causing directory and code regressions, the following rules should be appended to the agent's instructions (e.g., in `.agents/AGENTS.md` or system guidelines): + +### Rule 1: Git Conflict Rebase Mandate +> [!IMPORTANT] +> When updating a feature branch with `master`/`main` changes, always prefer `git rebase` over `git merge`. If conflicts arise due to renamed directories, do not manually delete folders or re-add root counterparts. Ensure files are modified in their new paths. + +### Rule 2: Complete Test Suite Verification +> [!WARNING] +> Never push conflict resolutions without running the full test suite. +> * If the workspace previously had $N$ passing tests, the resolved branch must have at least $N$ passing tests. +> * Confirm all files staged for deletion or addition are intentional using `git diff --stat origin/master`. + +### Rule 3: File Integrity & Verification Checklist +Add a post-conflict verification script step to the agent workflow: +```bash +# Verify no files were moved to root unexpectedly +git status | grep -E "renamed:|deleted:|new file:" +``` + +--- + +## 4. Proposed Instructions / Prompts for Bot Agents + +When tasking an agent with conflict resolution or merging, use a structured prompt like this: + +```markdown +You are resolving merge conflicts for the branch [branch_name]. + +1. Run `git fetch origin` and `git rebase origin/master`. +2. If conflicts arise, inspect whether any files were renamed/moved in master. Apply your changes to the renamed files in their new locations, rather than re-creating them at their old locations. +3. Once rebased, run the entire test suite: `pytest`. +4. Run `git diff --stat origin/master` and review every file addition/deletion. Verify that no tests or scripts are missing compared to master. +5. If any test files or core logic sections are missing, checkout the missing files from master and apply changes cleanly. +``` From 7c457f76cc610e227026c420a9a040ba226521de Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:24:30 +0200 Subject: [PATCH 04/22] ci: add aiofiles to dependencies and update test paths --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b8e5878..0fc98319 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,13 +23,13 @@ jobs: python-version: '3.11' - name: Install dependencies - run: pip install httpx==0.28.1 pytest pytest-asyncio anyio pyyaml fastapi "pydantic>=2.0,<3.0" uvicorn python-multipart asyncpg langfuse redis + run: pip install httpx==0.28.1 pytest pytest-asyncio anyio pyyaml fastapi "pydantic>=2.0,<3.0" uvicorn python-multipart asyncpg langfuse redis aiofiles - name: Run Unit Tests - run: CONFIG_PATH=router/config.yaml PYTHONPATH=.:router pytest --ignore=test_agy_behavior.py --ignore=test_agy_tiers.py + run: CONFIG_PATH=router/config.yaml PYTHONPATH=.:router pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py - name: Run Breaker Verification - run: python3 verify_breaker.py + run: PYTHONPATH=. python3 scripts/verification/verify_breaker.py - name: Run Integration Verification - run: python3 test_a2_verify.py + run: PYTHONPATH=.:router python3 tests/test_a2_verify.py From 4d8cea0321373517fc825e83a79f89e0f8999005 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:25:42 +0200 Subject: [PATCH 05/22] test: wrap test_agy_behavior.py execution in main block to prevent import-time crashes during pytest collection --- tests/test_agy_behavior.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_agy_behavior.py b/tests/test_agy_behavior.py index 6dade8f6..37eb1dd1 100644 --- a/tests/test_agy_behavior.py +++ b/tests/test_agy_behavior.py @@ -34,4 +34,5 @@ async def test(): if "RESOURCE_EXHAUSTED" in line or "quota" in line.lower(): print(f" {line.rstrip()}") -asyncio.run(test()) \ No newline at end of file +if __name__ == "__main__": + asyncio.run(test()) \ No newline at end of file From e45e7fc828ee95630260da4e6b8c9e99b181d042 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:27:32 +0200 Subject: [PATCH 06/22] chore: restore category sanitization security patch in memory_mcp.py and fix router/Dockerfile dependencies --- router/Dockerfile | 2 +- router/memory_mcp.py | 8 +- router/tests/test_memory_mcp.py | 288 ++++++++++++++++++++++++-------- 3 files changed, 224 insertions(+), 74 deletions(-) diff --git a/router/Dockerfile b/router/Dockerfile index cd0f3653..49d9c6a6 100644 --- a/router/Dockerfile +++ b/router/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.14-slim WORKDIR /app # Install deps in a single layer (no pip cache, no extra files) -RUN pip install --no-cache-dir fastapi "pydantic>=2.0,<3.0" uvicorn httpx pyyaml python-multipart asyncpg langfuse redis +RUN pip install --no-cache-dir fastapi "pydantic>=2.0,<3.0" uvicorn httpx pyyaml python-multipart asyncpg langfuse redis aiofiles # Copy all source in one layer — removes dead config COPY (volume-mounted at runtime) COPY main.py agy_proxy.py circuit_breaker.py aa_scores.json free_models_roster.json /app/ diff --git a/router/memory_mcp.py b/router/memory_mcp.py index f2187282..7ec87c26 100755 --- a/router/memory_mcp.py +++ b/router/memory_mcp.py @@ -17,6 +17,7 @@ import time import hashlib import httpx +import urllib.parse API_URL = "http://127.0.0.1:5000/v1/memory" PROTOCOL_VERSION = "2024-11-05" @@ -44,7 +45,8 @@ def _make_key(category: str, is_global: bool, data: str) -> str: # BLAKE2b: SOTA crypto hash, stdlib, faster than MD5, deterministic across restarts. # Provides uniqueness within the same millisecond. h = hashlib.blake2b((data + str(ts)).encode("utf-8"), digest_size=HASH_DIGEST_SIZE).hexdigest() - return f"{PREFIX}:{scope}:{category}::{ts}:{h}" + safe_category = urllib.parse.quote(category) + return f"{PREFIX}:{scope}:{safe_category}::{ts}:{h}" def _parse_key(key: str): @@ -53,7 +55,7 @@ def _parse_key(key: str): parts = key.split("::") prefix = parts[0].split(":") # memory:{scope}:{category} scope = prefix[1] if len(prefix) > 1 else "" - category = prefix[2] if len(prefix) > 2 else "" + category = urllib.parse.unquote(prefix[2]) if len(prefix) > 2 else "" ts_hash = parts[1] if len(parts) > 1 else "" ts = ts_hash.split(":")[0] if ts_hash else "" return {"scope": scope, "category": category, "timestamp": ts} @@ -68,6 +70,8 @@ def _parse_key(key: str): def _is_memory_key(key: str) -> bool: """Check if a key follows the memory:{scope}:{category}:: format.""" + if not isinstance(key, str): + return False return key.startswith(f"{PREFIX}:") diff --git a/router/tests/test_memory_mcp.py b/router/tests/test_memory_mcp.py index 0194dca9..8c93f1f6 100644 --- a/router/tests/test_memory_mcp.py +++ b/router/tests/test_memory_mcp.py @@ -1,11 +1,166 @@ -#!/usr/bin/env python3 -""" -Tests for memory_mcp.py -""" -import sys import json +import re +import sys +import time +from pathlib import Path + +# Dynamic project root discovery +root = Path(__file__).resolve() +while root.parent != root and not (root / ".git").exists(): + root = root.parent +sys.path.insert(0, str(root)) +sys.path.insert(0, str(root / "router")) + import pytest -from router.memory_mcp import _memory_entry, _parse_key, _parse_memory_value +from memory_mcp import ( + PREFIX, + SCOPE_GLOBAL, + SCOPE_LOCAL, + _make_key, + _memory_entry, + _memory_value, + _parse_key, + _parse_memory_value, +) + + +# ===================================================================== +# Tests from router/test_memory_mcp.py +# ===================================================================== + +def test_make_key_global(): + """Test generating a key for global scope.""" + category = "test_cat" + data = "test_data" + + before_ts = int(time.time() * 1000) + key = _make_key(category, True, data) + after_ts = int(time.time() * 1000) + + # Expected format: f"{PREFIX}:{scope}:{category}::{ts}:{h}" + assert key.startswith(f"{PREFIX}:{SCOPE_GLOBAL}:{category}::") + + # Extract timestamp and hash part + match = re.match(rf"^{PREFIX}:{SCOPE_GLOBAL}:{category}::(\d+):([a-f0-9]+)$", key) + assert match is not None, f"Key {key} does not match expected format" + + ts = int(match.group(1)) + h = match.group(2) + + assert before_ts <= ts <= after_ts + assert len(h) == 20 + + +def test_make_key_local(): + """Test generating a key for local scope.""" + category = "another_cat" + data = "more_data" + + before_ts = int(time.time() * 1000) + key = _make_key(category, False, data) + after_ts = int(time.time() * 1000) + + assert key.startswith(f"{PREFIX}:{SCOPE_LOCAL}:{category}::") + + match = re.match(rf"^{PREFIX}:{SCOPE_LOCAL}:{category}::(\d+):([a-f0-9]+)$", key) + assert match is not None, f"Key {key} does not match expected format" + + ts = int(match.group(1)) + h = match.group(2) + + assert before_ts <= ts <= after_ts + assert len(h) == 20 + + +def test_make_key_formatting_details(monkeypatch): + """Test the exact output formatting of _make_key using deterministic BLAKE2b.""" + # Mock time.time to return a predictable float so ts = 1620000000123 + monkeypatch.setattr(time, "time", lambda: 1620000000.123) + + # data="data", ts=1620000000123 -> blake2b("data1620000000123", digest_size=10) -> 5e5dad075ca7764bc51f + key1 = _make_key("cat1", True, "data") + assert key1 == f"{PREFIX}:{SCOPE_GLOBAL}:cat1::1620000000123:5e5dad075ca7764bc51f" + + key2 = _make_key("cat2", False, "data") + assert key2 == f"{PREFIX}:{SCOPE_LOCAL}:cat2::1620000000123:5e5dad075ca7764bc51f" + + +def test_make_key_determinism_and_uniqueness(): + """Test determinism for same inputs within same timestamp, and uniqueness across timestamps/data.""" + category = "test_cat" + data1 = "data1" + data2 = "data2" + + key1 = _make_key(category, True, data1) + time.sleep(0.002) + key2 = _make_key(category, True, data1) + key3 = _make_key(category, True, data2) + + # Uniqueness across data + assert key1 != key3 + + # Check determinism: if the timestamp parts are the same, the keys should be identical + ts1 = key1.split("::")[1].split(":")[0] + ts2 = key2.split("::")[1].split(":")[0] + if ts1 == ts2: + assert key1 == key2 + else: + # If timestamp is different, keys should be different + assert key1 != key2 + + +def test_memory_value_happy_path(): + """Test _memory_value with standard data and tags.""" + result = _memory_value("some data", ["tag1", "tag2"]) + parsed = json.loads(result) + assert parsed == {"data": "some data", "tags": ["tag1", "tag2"]} + + +def test_memory_value_missing_tags(): + """Test _memory_value when tags is None.""" + result = _memory_value("some data", None) + parsed = json.loads(result) + assert parsed == {"data": "some data", "tags": []} + + +def test_memory_value_unicode(): + """Test _memory_value properly handles unicode and ensure_ascii=False.""" + result = _memory_value("こんにちは", ["世界"]) + # If ensure_ascii=False, the unicode characters shouldn't be escaped (no \uXXXX) + assert "こんにちは" in result + assert "世界" in result + parsed = json.loads(result) + assert parsed == {"data": "こんにちは", "tags": ["世界"]} + + +def test_parse_memory_value_success(): + """Test _parse_memory_value successfully decodes valid JSON.""" + raw = '{"data": "info", "tags": ["a"]}' + result = _parse_memory_value(raw) + assert result == {"data": "info", "tags": ["a"]} + + +def test_parse_memory_value_invalid_json(): + """Test _parse_memory_value with invalid JSON.""" + result = _parse_memory_value("{invalid_json:") + assert result == {"data": "{invalid_json:", "tags": []} + + +def test_parse_memory_value_type_error(): + """Test _parse_memory_value with TypeError (e.g. passing None).""" + result = _parse_memory_value(None) + assert result == {"data": None, "tags": []} + + +def test_parse_memory_value_invalid_json_string(): + """Test _parse_memory_value with invalid JSON string.""" + result = _parse_memory_value("this is not a valid json string") + assert result == {"data": "this is not a valid json string", "tags": []} + + +# ===================================================================== +# Tests from test_memory_mcp.py (root) +# ===================================================================== def test_memory_entry_happy_path(): """Test correctly formatted and complete memory entry.""" @@ -28,6 +183,7 @@ def test_memory_entry_happy_path(): assert result["timestamp"] == "1689201948123" assert result["memory_id"] == "test_id_123" + def test_memory_entry_invalid_key(): """Test with a key that does not start with 'memory:'.""" lmem = { @@ -38,6 +194,7 @@ def test_memory_entry_invalid_key(): result = _memory_entry(lmem) assert result is None + def test_memory_entry_malformed_json_value(): """Test with malformed/string value where JSON parsing fails.""" valid_key = "memory:local:notes::1689201948123:a1b2c3d4e5f6" @@ -55,6 +212,7 @@ def test_memory_entry_malformed_json_value(): assert result["category"] == "notes" assert result["scope"] == "local" + def test_memory_entry_missing_fields(): """Test gracefully handling dictionaries with missing keys.""" # Missing 'value' and 'memory_id' @@ -78,90 +236,78 @@ def test_memory_entry_missing_fields(): result3 = _memory_entry({}) assert result3 is None -def test_parse_key_happy_path(): - """Test full standard key structure""" - key = "memory:local:code::20240101T120000Z:abc123hash" - result = _parse_key(key) - assert result == { - "scope": "local", - "category": "code", - "timestamp": "20240101T120000Z" - } - -def test_parse_key_missing_timestamp_hash(): - """Test key without the :: delimiter section""" - key = "memory:global:general" - result = _parse_key(key) - assert result == { - "scope": "global", - "category": "general", - "timestamp": "" - } -def test_parse_key_missing_category(): - """Test key with missing category""" - key = "memory:local::20240101T120000Z:abc123hash" +@pytest.mark.parametrize( + "key, expected", + [ + ( + "memory:local:code::20240101T120000Z:abc123hash", + {"scope": "local", "category": "code", "timestamp": "20240101T120000Z"}, + ), + ( + "memory:global:general", + {"scope": "global", "category": "general", "timestamp": ""}, + ), + ( + "memory:local::20240101T120000Z:abc123hash", + {"scope": "local", "category": "", "timestamp": "20240101T120000Z"}, + ), + ( + "memory", + {"scope": "", "category": "", "timestamp": ""}, + ), + ( + "", + {"scope": "", "category": "", "timestamp": ""}, + ), + ( + None, + {"scope": "", "category": "", "timestamp": ""}, + ), + ( + "memory:global:category:with:colons::20240101T120000Z:abc123hash", + {"scope": "global", "category": "category", "timestamp": "20240101T120000Z"}, + ), + ( + "memory:global:general::20240101T120000Z", + {"scope": "global", "category": "general", "timestamp": "20240101T120000Z"}, + ), + ], + ids=[ + "happy_path", + "missing_timestamp_hash", + "missing_category", + "missing_scope_and_category", + "empty_string", + "invalid_type", + "extra_colons_in_category", + "missing_hash_but_has_timestamp", + ] +) +def test_parse_key(key, expected): + """Test _parse_key with various valid and invalid formats.""" result = _parse_key(key) - # The split(":") on "memory:local" results in ["memory", "local"] length 2 - # So category should be "" - assert result == { - "scope": "local", - "category": "", - "timestamp": "20240101T120000Z" - } - -def test_parse_key_missing_scope_and_category(): - """Test minimal key prefix""" - key = "memory" - result = _parse_key(key) - assert result == { - "scope": "", - "category": "", - "timestamp": "" - } - -def test_parse_key_empty_string(): - """Test completely empty string""" - key = "" - result = _parse_key(key) - assert result == { - "scope": "", - "category": "", - "timestamp": "" - } - -def test_parse_key_invalid_type(): - """Test handling of an invalid type that triggers the exception branch""" - key = None - result = _parse_key(key) - assert result == { - "scope": "", - "category": "", - "timestamp": "" - } + assert result == expected def test_parse_memory_value_valid_json(): raw_data = json.dumps({"data": "some data", "tags": ["tag1", "tag2"]}) result = _parse_memory_value(raw_data) assert result == {"data": "some data", "tags": ["tag1", "tag2"]} + def test_parse_memory_value_invalid_json(): raw_data = "this is not json" result = _parse_memory_value(raw_data) assert result == {"data": "this is not json", "tags": []} + def test_parse_memory_value_type_error(): - # json.loads will raise TypeError if given something that isn't str, bytes, or bytearray raw_data = 12345 result = _parse_memory_value(raw_data) # type: ignore[arg-type] assert result == {"data": 12345, "tags": []} + def test_parse_memory_value_non_dict_json(): - # If the input is valid JSON but not a dictionary, it currently returns the parsed non-dict value, - # which violates the dict return type annotation and can cause downstream KeyErrors/TypeErrors. raw_data = '"just a string"' result = _parse_memory_value(raw_data) assert result == "just a string" - -if __name__ == "__main__": - sys.exit(pytest.main(["-v", __file__])) From 36fa94a0fde0d19f831c6f353385bd331449c6c1 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:44:18 +0200 Subject: [PATCH 07/22] chore: address review feedback, restore async agy log reading and classifier thread pool optimization --- debug-homepage.png | Bin 0 -> 310637 bytes router/agy_proxy.py | 15 +++--- router/tests/test_agy_proxy.py | 41 ++++++++------- router/tests/test_detect_active_tool.py | 2 +- scripts/benchmark_classifier.py | 65 ++++++++++++++++-------- scripts/get_pr_status.py | 2 +- start-stack.sh | 4 +- tests/test_get_pr_status.py | 17 +++++++ 8 files changed, 97 insertions(+), 49 deletions(-) create mode 100644 debug-homepage.png diff --git a/debug-homepage.png b/debug-homepage.png new file mode 100644 index 0000000000000000000000000000000000000000..8a45fdc26b25534298e44d04c12f99426641139b GIT binary patch literal 310637 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

=%ml#{3p1^?xfI^V|ke zBka!e)h~$p;y#1>q?+Y}b%>-XI-LHHONJ!Wy6gT3r|jZ0Q}m!1wCox~JhIK>s+-+z zbo^cRbnx8Nb}D7Z@IMj12EIBz7Q%^tSwd5-Y<@oUcKKjl%n*-SgD0rr8SOEAtXeRt zG{thot%yCWpV?kN<}$pce^jTK0pHL-NU2xYmN6ys=E>%30KluYY~|8HLq_1x7Z(*k zlzF`Vl`B`{X#;Ql(q+pB4jKqS6$;juuPGZbY-mkQ^^V(*3aiHa#mg5gTb-2=8a{Yn zeNHxnZ~5Aafc+pDSX@+7Suu0w%z}b~B!2*UJug2O(SU-&qIvV?l~t@&+@57CR^sWF zmX=XnUCp0!iYp44Yy|C2SssroHN_XL(qvw7pT*;v%Pr$$gb{er8&NK)Y*@ake4k09 zEIyr<7>v{=`H~bDca{f(L9fTl2h>_Tua{5L%O9blGeIx@38G}{MK0eU@I2fBEb{RP zwhNd~E++Cc6MU08bka6FX6I~>g($wD$7hy z|5miHAkQTUircFhtZGeJFdR!t3anjQF`&4J-yvkrk|nFq z*}NRT9+z+}*45VFM&j|N>#;bjD!#W;lDv|k@(GCJSfd-Ope|IBXD!mtl(g{V9mn$au1IHO{@Ox-&9X z6suI#!v|jTI+!N+;&X?)uBxd44~#Rci${Az!6h*_FIprBjhRT?DBx+>P-%i?*9hWA z)UojxbBZzvkW7N@*KmrTT8;HYg{CQlO(&z+9VtI+!MdvI(iJPDU;Z{zwjB+y{r&a; zcjqc=Dwx%j$-{NOnRwIOQ)`zSwi3xiCX)?G5t+61Im2L7;>9)L<%$*={gXljAT16ew-hRP?$rpRRXL27Vgy=%C35 zGcs4qUB4o%=&mS27WPOL-FD%@>lPPvml}xtX}AO>Fx(s8LMCL`JlTvR0R0LoYu2tV zDS>Sr57(uprZ7`aNeSfU<->Chd8WleeqTV6U3oGiw`MO`~Y z_-pTfxx?7OFm+RN3#Lw;QIwNbl$V*4Hn_67rle$z7B}+qvMbkCtu8C|`2uZ#c*#|*VJa`DDamQcNhQlmqPmfnT{v&?(vidc0iT!8 z+8tHl5sAfdqm;b+6CD*Ld-DQ4Gg*x67Ut89P>s^VyO4e&d6o-EJAC z2lkqW=l0|1q}J3o$YQcRX9FxH-2c3)5pM`L!13Yn1)Q56gfh=VkYu-DBFb!f7V2 z)i3ig8;;87`Tcwxh@G!w7p-uFKKGfTLw1!CyL?Vl;)5hee0E{}v~g+`^0|!>X+h6< z%{#Zm%@Ll1r1E?uJ~xA~F5}2aB5HFOuD~n`0Q400*$~mR!u-6#QNu$OYa%r@{=$Kt z>};rw(6Ys$#S3DkCAdxjqUQ@0FzIzJVrw`%Y#pjfD)G}WJhdx@%xxD6%Y?DV(-$;lbn*&?pVfS*r)1w$G} zHlNfluEk?^cz{4o?XqpGGxOn5jVQ9eRmkMSPj%r}~ZKTo&U9qnz`-{*;k&`ONY zO)lBNED7cXkM}7aF)3L}N%d!ErzIyxBGKxaY6Ni8Qq!}1eq8XGZlyRaeQZIVIdpJ6 z+$C`>rW-YuUD-$RDB0fP{QM-`aKjUR`SFWjYZS2f((e>0M;9`6D_<|p8Bcrq)f(q%|3SW1KHM$EJErsPpZeN zF-@G4s?NktIX>c&s%Vx+a!2e!u`b;$l=>?EtW4fDV0kpJ>_Z5g&p5XEc~Ri?LZrjs zSCPeiR($+7LdT|s=XzXnBhU^vme@4&W1|hY32Yf&Z;3w;VSqEmRLlmeVP!a=C+V)Z zWb&#cx}e$dcKiUvG8H2nFP~M>FOVJY7iL6v*?bLvZ+`uK_lev5@YC-@h7GGX`j~&CAV8 z_J?#eR9KX^VBwGi_s^AIkW{jIW%=4zLtSwAu>5d?x@6h1!9)8sghFNIHADLk{Ns-$J8awE z=MP{<;_4K==n!_W?iL|rxkJ+#4l4`%4vZfNsmha)yqepW6pA;bW@gQrxy0u;(vy>T z*>b^5%}P_;3Ll;kiC0zE;1~AqmuqM`A8f6;>mzkOLGXJZzv0sHsJbTe1VLtKDqM+f zXIfPci^aKDQB-*557c%<(^VYESQt`X=gB@&I25d{3#TRfpqX*g^LdlwA$~=~H6@0C zIU*@SOlCUsx}vI4TNlj9#4|IFH6#VR8uKVT^9O-p?0O**iQy&&Z)r3VLYT;h_=GGq zj;ysazWDU5`~aKwjK)0Ok`KFvW7VzbW+E4;F$f^T-OFnVIca0u;^2jj3?tbk8+@3E zVHZN>h9Vyz%-6s_c1ws?L{pPIip(s zjiz1x(DtBiF2ceJL>-Kx*1?jiZCt z<#`BZVxe8{pEVAUabf_bT?E+LN)zGv43d^@12D;3C)IrsDVUU|=0qZ5+!S?QkAU^^ zTu;4?X5qFSR);HP5OPb+YN&+0_uJ^@;X; z8x}wHH6d<@8g97)v<2J5HEK+WJIJNRP#?OeJ0(mNXg{|VXHovCDf7gCXXKniuMcSE zgD4c{R$T}SdK;4KCWn1NzasG1xFwl(fj;|o$gNbo=9NEu;qfOGju=yGQhwQ)4uRs6dOG~S%ttrk+PwSTjuY65?sIES|+b)yVR+c9v z`)B<*XF&gA=)mnpjrF+Q+ix?Xa&1jG%%34eg}I7X+-K*Bv*#_IGk>Wmu1rt$_s`EO zt*VwI#>k;V7Oz-7uWY5fy3_+T%JX472rQ_YhO1z8*_!mUUE&%gW5; z^`Uw4x}4<4Iz&E(7cvoU6kgt5&@~laX_sjw`;y>CgY(trPpPX7>4sWZm|s;@1@)g4 zNQy+^L@?mWhP-Pk3TZic+s;f|%z3ss)ni!WEL4sz#PgdM)8Sd^JSBI)z@oqYS}=V0 zUka`F{2FJ zJ;bC?7+w@@n~~Y8%c_ctG6lnr+n;3d5e|If5w{zihyRfeeqeSEA9L9$PXES+<^M}29c`;uOch)xE>D1>gqx+x4WPq-{jLKh?XSdM`6t#MS{077L#0l zFMRhr+uu^*YgG9N1QyY7&yyO0W;mk6Vq9m}R)_xnAt@a&`M%tPtK4V)v((=mC8A!g?oIPNFQu6+DAp~r_oG{Pr1bLt4$!b)`6 zm1LpNVu+Q*RXa{#*j0e|M0HUvoEp8GUY8r^q{97NCd^d+W!uH+WPh}{+=QlEez%|h zt)@I~k9Au}44T_SUZ-wiwMvx`?5sX4E$CO6nv;cn8H>dfUm$7VaHdGH`l{fvCE-;o zw3-@TUYr;5QtW!Qc0C{N`;XyVlryrV+DV&z#f5X*B3JRNOLFspTFg^ z2sEYx*jY2o4jvgzwRW6Qqt079-G-0G6%5-^!A%hzXQj>*kFTJppr|M-D=Qs-l)T)m z^mM<^C-50X_)sKX=2(qK!ojsQmC<;-F`^(UJlP15P{oZ{AHUKi9`aJ`kuSWPcKI;& zx9PQ;t_=oF!}H=SN8>k*5NI5X?3e&%A{WlF8tisvvvW@n5<`!-dOTI$su{t+cuCI0 zBPO65Q9;@Y6Q7zRF$oW9DMhgqlZEZd3f;~1k`15ABKqpM`Mc@g(XS=L#^)38nYh{V z+Z@6Mit9}5-FRNWQ!)Xc7n*PA}k*X&|!3{neDl#WmQV~5NwIrN?GpX9R& zgcyrLu{3+C5bJ02H2|v1R>5((a@Fb`$Bn3~t?gIbe}^6V&z-j*;P-s`#aFn#h7K8+ zou1)_4<3PPJrdL6!MX;7h+_i$%AS(dWxxMXk>-&n>^v!_An%tczX_I>mz9~7mKh31 z{#v|9@pycm04$2~%DUpBJP2z@E1lPQ=kBknn!4QF-1@pYpCAA8g+n2D=xb}LTrM|) z*9sm>OpF`$Y)b#n-hTyKmYr#0*k0SaGS$?@9}1Pr|(~D zpPN~Q0#I2f00}fsB`!{$JolWv*Is+A@BhC4|9%{0Ay>(=lZSQ#@>w~G?SW2lZ%K%{M`XZ9WOL`c%RT7PG$VyYT zZGCmEi`foA06d~MUtqYNztJY{jQj$rSUF0-Lg=)$YEr%7IIaZ(`SfHooB0lP52}oY zaqqNmn7OW7^V#gijVr_&$BC@yS;sM{CIA2s07*naRH5_-L!fuVvaej(!DvjUQ$$MZ zYfZwPNp7mj&gSa=;8ZKjOfrWfuiLIj%t|Alc4=;U!(|H%!x04`W$fTIO& zFPMR=m()3xAmalOW*`hRO@n%oApn^9v7)F!5H#v_>P5ht>&^qRd8(LIKn;ts7gWdA zc`x3=)JZ;Y5pWRXQG)~@SuCP#9xZAW(k17!&s`lXAs71FAJ z7$Cx*EQBH%W(W^(OC0Vk?%v6~1*PSv6nIFgEF4QHY9R}QjB{%_6AlBJh-E|=S(M&8 zJY4yZfz_D?!7EBg=0lh+Yt<_+eD1{;E}TEVy1v$^RWYv?K{%Mt-#$5+L7hm`5F*@o z91KpA$sqNE6bk8lk|z{=a6e9sGLkKmxfQF{aPcACm3C*PTdm?gu0je_$#oGQ%iVj) zG^8l=dkRwAIHJqTM27NcKMe27dH^2VEd7dvKM=o3$!_?s2Y@gXJ=`y5Sy21B9X28& z{|ot-yBHXRO`=4sQ51HnEGQ2K>*c2->3OCYGb1y*m(TU6_N7Xo&!I5zIjMtFr^;L2 zicb2~;}bkcBu7}Lq>~3;c3-xc!*W(E)wm@;#W=pltuNe)6r!|7%H-{N)o`U2guz0~ z3@Rn~)u?~6$I}}CtYO*T*!uqe^8>)n7hn2Z?`W^ze?5+a-}v?4s8=hd+x{27_xtTu z%`)|8p1Bf)N1NN*ckkXiIy!vjnH|S9y#zqnP9*l1fBBcys`-UyFYg_Vzx(?88!N5u zTK9O+hnm0&FiqH0^!dUA71n4pqA0}Fsn_dA$0xwP-A?YPH7W$$T+yHforu zvxV1a)XCkFc2+5;dR&h3xx)rwWlqKM0#ixAU#wJ798n)9KJ)R~##Y1k>#KpontZiU{fqD1x_sf{ z2M_lfEhzb^VnEQxsOXvw#7=1{QB0kWDWs9hbHP>~#Tad7Rjl3paeJlmV1J;=^ObJ1 z(_T?z!X`-)fy2&uLonzbAvfX-;t(L4xh?`W-Ew^2i=rUPwAm~Gm^nEa;goSHt+u96 z2D~;NPvh9nGG+jgDT?pU0h%pc))Y|YauOHwFpJ~3+tvX0vm!7Q87PBN8n}lGL>YxD zH#F!qMrI&{nE8QaJF`iW#lA|J{)h<|6-TR8U0#li!C}1BF*oU_4$Y7*5DaKkAIz5~ zJ|!}$C!^8>rttm*Ot>8I4MQkGBJdLu5c!`dieVVk*OhmNqE^R<)B@sCF%YVzLwxAZ z18SinD+yvzRi-}_mts4jA<8d6SO@z-2*v??8Qm%X$LNqXGvQFP&u={yOea6xDv+ylmLgO&)l|-I$sY5YU#bs_A+xY8 z#fCMdfZ-6|F)#Ru|t3Ti>Yp z)tRIfzZ&yT{&;!=fJ&|L*4yuW>80l;qd~<~>(4y*U;fko#t+T)&aZ*=%-d$DL7^?A!~nP zia~9MS;`Td><6J+LEK?>SGp(teybvHY^)_oOg*PSDBCt5Jy`#4w>6neX7d?>0pNSE z-L+a3+ObbT6-NE!6-n5o7`ms=7*fdfXS1dhv##tTSC#fq~Olx_tSZWf#xY zFTL~b-CMVJpSgNo(}1C=Hoze3PauK}AQg_0pMVyble{zJF*Ql;Luw7goUUTK7q znTGf*%0r1f?iFJGB;rl6S9@^)=CePXo(v}hcZ$rZcE{7XDi_x-U8q(p1Seq{cp-7F zwq}HW8YR@Gw^~uV%~gC9RbB$A7lm$HET9I;vSH%m!K@P`;=m3;7~`(tnnvB=bxG26 zKsXn_$m<{_*Kt$oj1aaOE+invRI2TE99(w++=cHaM;f@Yj9jDn@gpRF_!CenGqY@i zr}^+Uz8_BdI_n!iRRJ`ByLK!iL#vJ-1(HUnDesqfJ)2}1L!RG zFq3Eyc^6>$2iiKGIfb4*eX>U>DHK0p649f>`H?R^4Jf7HMo*PmWn*)_vD)>5#pJYa zw^r?T7x!Z@pG7UT?^ z5#Z1A#~{97z2!%S|DqJVC>}G0r`t9w_f>Kt6E0VZVab(hv9e}tpEp-mgChOl^;h2x zgOTsyUoG9J%TgooCJ=B>PxZyv$r(cAs>JA4V!7g)R472T3;FA@?33F%1;pgD%*#=k zX2>`lu34#rQPLltzVT*HVPEYZT5=gCmd8{&dJ>(RFOvq&dIRv7qL76KCeBHe`Lfaq zj9WE}+JIcdLQ7lJU+M?7T}@O;jUE&;=2yO8B$fD=8$IRZbT5OLH`ny}9nURlpW5w` z?hnd6#jTqek)A$CeJ5#N=}z?_byzE~i7RQ5{wf{i6Fwl$#4Oed#cpVxkwB>-bYD7^ zl}=4u2jKIw;1mY^q3QrDHIw6b@SSn(OLkaaNXs6^zk2-G$I}}C{NW#e^QG&Ty(l)c z)ra>UdSR$j!zZoUUj5eV?|ZS-X?N$lhkyI)FL`0KzkfJg%vM^pB#dPR+h1^ebmY{U zNgl3mZF_U?@Wh|akKBrV>D;#O`TNI*txhNIpU&sgTD=Ld9t1uBdaYi;eLEZuZQV$s zXyp4lJKMl>$Gwx4RvVjayG@0>c@jI6ip?z7Hf?k6c~X|uB-gdv-pTmH)z_L8U7yry z6~}aF*G;p6d_1XHnd4Ytn2tsh00qqM;c!GXqqZ#&zvw3=@y@d1iNsUqC4MWUJQbeX z`bs)AzXG8s8M?Z*%6ccKls!le5J6G$w zcZWCM+k5WDImeZoP0jbiPPbaCTl;$lE32zO)w9_Gc=P&|Rh1QCkYF{terfCOgNM&v zJD;UFkR@U7A_ww)W_$I{{wN^}PW8b-R58$1`v=C$~A{Hf|2b8I=WcTkq+}vD8h#|$3 zYZq5>Fs5n9GJ*RdNnDjuh~pqaNKh#Ha5P-4cc}pYueH!*g2+W0M_~|B>ou=Y0rgK! z7HDCo$`C!{&Zz(hDqu8kuocGX)@p0E2x1mOS;8-dQ_oO!C<=rwh^VF0m3c2AaY4__ zJ$Zm9^R{1P#Q=~78q;CAs(>NQdH=(uqZ##QQ-o(Ih<@=o5_N0Oc`E>7k;&MFJDY1H z2T}F|0w;_!B7&T&NJ&O9m?Bdh6^4zkmn0tWo&+BTH)a0AGCBH5A3s~P^z&WXDPt?k z8dZ1e+Ev9i;>B#x>lw{<^ZbrsnSS68AMW}GyWaj@HX)Lmi6U-^+k5dedm7_}!)eUJ z!I5T}*4hTS^Tlxfc!AU5eH=L6Y z_UF8>UPKM>3KETEUKqQ~>XAgwOBwJ9;X#<_oaOy2i)cA7&M^06o&%Iq55H<%tyG|X zZYc6^T9P?@;12KUn0{G~!*{8@iJ}In5eC&Pw-L&(5r<<@SE3x#R7+#BMI+~aXcFXD z`sO13FGKlXCdNXMT`>^_aKe2@;jSdwi_tOCjY64F0;q~}DB(X3RCur>#0#!?iPjoL z*^N!SU-;{maYD|i$D+&zFSwS%e%{mUXYioaIi}drQPk-!7>>|=NzI&J@&G_Bs4u79 zQerbK(yFkYl%D*dByJ{0Sk18(r$$@^%HrK9&Qjw=Th=0S-bsW|CjIL1i5|Fbu}8(S z)=>NUL}R8{DtV&c^8OZD9z8khBADTlB3w{qHQ&o-EO0(=XN~A*U&Q=m790(nqJyUw>z;sZxHh+HQ`= z6F*Eem&H*spZ6QJhGrOk|D-<}b=#dNO(wnuSlDf4X|Y(0C$p7ylZ+ta zKm%DWNe}i9K)YgxxOM;Dxt%S*(CUD12LJ#N07*naRC>L#w}0q3CL{q+=BlREDsJd| zf$zDs#>1l%)7Go?szSyuvQw!k31!z(Si+5Lb&Y-nasj^1wm>Jx)7e~BR7=4Rg}1UI zuX+x4RdVg1jJKS(+>u4o7W#Ky14A81WHljBjg==t+){NMO;a0{>KDH7((QY9hoc$D zX_fkLF))b08wx4*Sha9_8C#p}xD)9HwUes3HEfj{C6=ojTkGB;kcw=jTO(&uZjP_% zM0ivk+t4N_y>YW;MVZF{{^_^WMx$~`)$Z>eCb4f?joxHjXpQZaikv0jF_~S7IWMPx zsyDGd;+Un`Vln8C-Ky=_+U45zoqPMux*hpSe;lqf3|7QNo*1%}8UXxMZ;>QYLeLol zpgER-sL#>$FFb$Iu`E6`DH!_@=g5R(K7;Inr2^O5u(X7dy|mLbf?}gkoo+RuE{}+Mbs~!sKMJw8SeD82l7PUdnSxH9pFHzB<=qVm27u_88qFWD$weW25G}R96~K!M zZyrY|QK6&@`Wo&+D;hs@`xB|mHu|)WUwX2OcZ$ew+_>I-?pp8Qp?`d+RXepSS4`K& z*iHtg{@w$Be>XloNxguCEKV)sZkBnGFtPOn-RTad8W`Nzh}Z`MEC*VpD(j}wSi!xd zRVs>U(l3Ty76tKml1xU?Xp~IH;b4$0yrOJ+T}m$mka3TbB|**|uu@(RkkNPMN$PpL zM-~++N{@VXM2YM&;-lHfiE!zRK9@v(6F2!+srw%$rEWT#TlU5#`HuE`!`j z{;jWm?wfz{txBsio{aC`yLaW;7tb@fxBqZ$>)f6FezQ?;HmhHL={ZoI?|pFRWawkA ztaMkt`@J_m|GDRaFv1RSdV1_s97Wg0^V$0PTApUTejhCB`q~;cllg4Awy~~i>hbX* z2%wAS&c#XS2cgrgO-3V40ZFU2JDn(s8TQ^duGltMSy=(+m5@(-N*Ns~Irm4{T8>Av zdxt%HKswwH)WD*Kam!I1v!a z^v;6uGFGy}STmz)w)us~fo>^2}FdFnu2DL_eb7v<_7P_HjDH+yJC(}l?{`%|hZGHV^O{Vl9Y|*CX%6XWQ7?9vX zn5L!b8gwCs0KhUE6;J`xCnQY@TutoTr~N^z)lTCSL!RUr2HmzbGAc?XO);=+*Ib)% z2@z2PAXuhUnBrr7iFt2y#k6qYT^G`U3e1i92M7XOPZi3V4s|I^i|JzFG}-|A^x>A` zR`lu2Q&p{ZGOX1gGMTteK@?*;0RjR{Ajni?;RPh;lwSlr!NB?PmSjNY3D<_8T~L#_ zFbsJyC*m3MEn=LnHxcA$g|LMZ$zYjCRS;cGS=m~Qkuj)3nM9-;h%zavlS2GLMnGJU z;doSDHc1jiRs|QVDSt^SvyguE_|Sv9PDr|GthAcvp1B+p*~7P9hn{9_U2@xP$VT(& zWN~~D^$w$x9`JhVEdaa;lv0mq9&Hgr&wDsCZpE2rjJM%Y?V5A-`P%jCifzi4rSSS- z>Zmt9jmD#RGEAp4a`g8Q2}H3^enFUuAIfMK+3QKlD*#f7Hzna|KL|6Dg1ds|34SXr zM93%cU{uv4UXH`beqIwyc{VCj4M>U-MD*ms3^4d#p-2Cd7zOsGwf?lf&w<2E7E0;yMJgu>miP%|fF@G($Xv!@UZm#6T zzg%1W=SThjdvEr}c!2{@%jOuOT^G*-rccH26>?rC0m$6XlUEE9lqjUKc98e=mUyNH zR{;3uV16rQVgUjh?ij=+8A&eFfePN*`>#U)@TfMrpjLti24;6W2aC-Mj%Q5eOwX{9 zYU#(c2>gXzA`~ho%VQ~qjM`r9gorXfy9F=Rq$;b;-<(=t%dT5V9*}58eK|12um|WQ zIIGz&e{spDdVv38WVszNMXRp4ll!6i$y)qU+MgtY^8l}jK|XzWVA*>2^Yy7QpxnlU zpadk*pSvIH)A@LM0|4xCFTC)Q|JvJF@W1uF*Is$$>)(FkojdoBfBmap{-bYxr&=|u zmip|@IpDMV5BBdr+;2APo9k=MMypb7y#3zIlm6-E4&{YcDwV_I<1h>^U%3Lh{lVeE zz1w#z-CSE+Kb?+1!yB?1g#K{Yry@(;8jt4Fh6iqV3Vc1D0T>mEY*h3{t$K8DxW2lQ zs`xM;n0q{5G+Pajv%>JF*=z!NgF?mz9A?VE&rZfO-O`KPb`|WQ)aaU~tH8^&+iKlz z_voK@SPYi-i|B{!w5(K{~HcQtE;Q^S~X1*kuOBH zo2H46ozA@n58iy`>tDA_{phGytvFc{fIrTtp%rkl%Cp8(Z0p0JKN!w%Fs*KVG#=M$ z74oRbGu>1>X1m$0U^Eu#@@Cp90Pz4@cilN z*xl?prVhS6@S@YP2<%MRv5apo`bFf8II zC#QcwSFzH=SB$zS_$A|=49Hk(wv0{_Atp4NWHAk9({u*De-ck8!2P(}^EfV2PTu1V z2g+Ys1(Zj~b9JauR}@lNu%dFOP*HutQ`!+N;40fL;a&uS!2PPmbVD{Rg8M)!qR@qs zhswn=;7#9oefI8K(eV+&8)6|4t|-b-lYW05e(w+e^y}}u z_sZv9*tm8DpD&1izJjsR-m3gaR@UxvhMB201Vl;GCF*lbX5=7A{`D_de?yZ0#a{0} z{`0?j?_{8B>QV*ZRS8t*Tk!J0@^eMWFXEeYITb|_Lz(1rCU`7M8qT6$#?JBzmJbTh zsFDH$%8M%$jc5LeiTz8N9K(qNT0J0rDxMv@>tkVcZa5K8vY{j_R&wnJe(4u=Bvc5$ zSP`iTO12tqQjOSzeOAkiOr7R6J8T5+%}s+@J66bIYFC*8o)#(zbwxmN)vvOGKITDD zw+b*%tt_i+wcPdd#V0C9Q>aYToM01i+3Z@>Fqqhie$i(0j6>9T98j&06n^H;v|xqtTie^RSez3`2z7dD^0x-**k zN2kO7yLa%u;nDup3tLBn@g#^ce=Zem-7v970JQ@??X0a0^zmRgY~)VctpR#6*QqzE zz}cW=tCfnOn@7FgTDJ)e;b8v&H>+;BlkpTcc(c_y>GvD83IH@T0aGQGx?Zc{2!Xt* z4Xv(H<*33Aj{Dfny6uKNo?<7mOp_N?6kpV8wn5eo2V)%40$d~I1QSXohMp$`Bkb8Zw}l;+JLt#$ zViCkXKq!J4$I*EWE(Iiqhdsu$PPcXM!NbK~(5yNL032${I|%~6QFHN)t81(H)c#5uIA7Ov zKZ-F#mW=>}`e9+nD90ve)mr1^%+ zCg&TSMjXs##YhTev6$m};gDRnig^-v!C>MQjf#gL&yQ@f3d%K>)LaK}3`~-t%bivg zxPReg7y|qc8ze@OIw_?YwGviU0e|9zm{IZpHMP8~<)w&>Y9U2G;5>yauYrYBbm3zn zQYqp%%5cRLvPtJroJ8RylM_z!gFDH&%Rfe$LY>IR$WV+)VGfJZ91_$lh|9$1(qTqq z01-$f+ja7#R}uo3)Sp3m!pGyw^k)nWey&_&M#Or0etYZOjc2{koAr*1BvUI5qt#Yj z7XUXL_2a>5+#etuPiNCC2nx{c8R2ySQYGH9W63|WVp+^nx=^ZA_nVA|^I%paNj#rt z_(K)zRKb$-lpaa^ju3oATR2fzA`VextYdnFJ|qIjg%VYnE5dAx$a1Jo+m}>KCoUUO zCDL*d{HQbMCca6QZ9Vtw=Jjhh$liDVa_}d=7mcQrQ?EnPwDEB2339q#qNA?> z^`-w$&O4>CWj=P9{3LVvjAt2B9fDkD0bwn(jQaobAO0I(Q?1>@qi?={=fUv_#)8pu z%sqR5nFO+BAw*dh&0{Jhk-c{#Rdu!Nnx`*-|9_c?Q|P{s!+1@Jl&G=XlJZIMnc;Dk z2ZgW6wl=c8?@a4b#W`=r=|Z8bONzxIE+s{%f2jTPuO8(i=hptT-mxrkV!flLUZNF0 z9-u1Alt1q4B?DPNEcfrm%q;Ae?72QwWgS5RcMko?PW7uu>?sZaRMR=?kGI!4Sn$Ef z4vzM(?5v%f_TPH*jaR<-`HH1a=ZoQJ^ziWb+{W7W#>#5VYIIij4-RkN?Ok!J8>?M+ zI`U0@IOun}T}?ON|KRSq?e$i@8H9mRaiSpDS=$6)+CMq2fh2bv>YP;+rfFbe%)LN2 zjJ37ZlU~nTU}aQp-gt84AI-h8)GyNq2VO=BD$0F6pcCxxsjo9msrpWc1ASFgC^Svnk!JDtv2w;lR( zO4gSUH~BmiQtH(jnJGnqAA&5APluzxU#PZIkh{N{laD32_W%Ja)YFVx7^;e46j~6f zJRZPXvN(}d-SYzK0iYNmg#;$0^0UIU)P!J0Y#7WWXBCi+lrRv*2uG~Bm#J*tZ91l$ zcG@)nnuGof&;Q zc)llcABLlGvr$t3fT;45aKGpZAj=fUakp7UxfvOi8!L>i8t-gMts63`-ldR9<5LB{ zg=jP7ZBI$^$Z~~u6QJNmqx^%tUyo zDBRD2D$D_Jblql8zABy(HNPCZO4+x8$xgL${kiLM#Xi`*D}}M@)~wZz3^pCA{&bj* zM)B}8?4JVqr&t7{nCg9nXC`-{D4~@|DB)_JD2SK1Rg)O2Xg*Csk8;|<;>W4L_oC(H z*}?F%`ixr#K?BR^*UO4kl9fp00ajjkc@*YUw#2C7p2&4g;y$=Jf6jQS3zOuJ^K5;z zF$z0S{8oK^?fRF$)NVJA?|g9UzyBxmci+QuCmW`0nY7W$y!$WD_|N#1BKsYM4W#t{ zw-}H9@Z0~Wvn^XEN&h4nfs8xNdiRQZGpgDHfalXLUy|~2$;V4)b+;Ms>7}Sr;d7LUl^C zlF8I0OO>_2^sWKw zH?{z3pTBnLPrvngy;%hWx_S5E+c$5mc3R!hXk~5lH-7VPy#D=Hbyae0ePeY!41<%C z-p1zU&iQSK0A3Vz+Z|2U=X395JOo*XzwYlJnugJ8G-8l%fe(B&n#^ajNvGLdU0WNC z$A)F8`e;0x8M@~Ap=+D2VFHz|bh{4^PR6rEx6^iA_wcCa2XV7zfT!;dM(^L-+gN!a zjMEsOE63zMNoxc%fU|6eSA}N6C8kn!`~B0Em6dwE4r(8<0SNX=trqxkrBcJ2XYfQS`>UOc@s19XA*RkkP6~VF* z1n`?|S=fn<`Fth}VF?xGWdmtRv|HmzM%06bLmJW($WSro<&KQXsLPU1`X0`*NC-r^ z&k$!%dD;)6N-7$SpEw2RV;(;SGkhow_HhlK{g4m)nQfZ@=0MHWW@G!@w$Z50$D`nQ zSJ5oJyP>)j3}v#IrqgjcJdG#gXf&pD|0pa-JQ0R7CBe?q&qXC|F0!f$VI4|MgL$bg zUP%-Dk$nDd;-xcd%Q7k>)}P+ZQ-COJz^Y-|WF}Q>$dxJs;z#V!KTfeU2?{xRNPFS*&OiRg1}kkinJUpf-dI}mJw`iAys1z5d8`EJa#_1Vb<7ys zR8`6xD&Edom4&wt=lwXamrIIY*m{2G1bLk_TQ(0`CYA9;xGX|gGWsI&!TTcBcsbP&#GP)ni zl(|PN#YrG|=n|`;zW8=j1&MkQtWx>|zxs=cVkrw>lhs0%!O3g6+EB38(hc}oJhWVs zWEGNDE zOx7u6p}`rE0-U9sNqR0hzV5v_Q*}q%$zvtvd3?Vb`Hw!H-T;6WXGO!77Kf>%+Wqm2 zP3LivKiu!FcUt}7_}sY-IZM~p)+USKbTpq&=V5=gvLWx?x%uUnZ}dmgx8J{As1|5n zZ0e`|0rm+%z{8UhZ15_RtBppGL~#&nX|cVvb?5GVpm~j&dr?+PqfwvD7K6z|qP!Dz zKATo6?sPJ*R&9XwlT&<}1i(EQj&(z;xc0(Z7`h7lRs?Ypr|p(~@!a|O zN*f$C1aNY2#FmqDOBO&q&hirVi|3|g?(gqzZf#+8H4M|To%tek-RgKe0{$~i_58UV zZorMRNuw~*2xuz^03-qbcy5VE?7$MInMM?bxRodoXnhSNI^r3;4FD5iM!Vfki^YR` zLtS&m!-+Ee9Ws)7suzJLw%Aot$=Bb$}p+QtDF221Bh6 z$J14{ZW*{tA$Gtli*xQ&GXu35N6%lsGM;%j&iQPCxWb=~Lh4^c<)$(&jX?!^X_5o6 z;K&hHosOox0YX2oNbRcA&}1$013zVh;he1S6$KHT%m%ZCU#Zt9t*AgG5n>p!LIHNE zR4Rs3jZ+$iLCtr@P*2H7m(hw|+Vcbs1u<1>}qC zC#zAT;wqYg01~Lb*=Wq>VY5-kMV(JhcQ)53(<$YSooR~qQzb)^D0D5bU5=_P-h7yQQrp{;s=;g3?8rFEf_Z+rI>`^5fxu^0yw31H9Q|(BqH&|6a*Cx zbOn{_3#g%3?ppXdH3(zwvG}BSDsLpbM0Le7vG}L=_z~p+^2fdN?36wd+$fK|O?1%O zTIqH!p3_V%n2aZPdbpERr=bICt4iw6(#0Yf527I%@?@TuhJHZ}tqbnfC3AjJ5?)GD zsRk6nRi-E(0%-~gKNl^O&vN=j!b%~0qPW2>;eHG)0#?%`+;3PCqJ5Q$Ukcu#g7BM4 z3K&%h`vESbsu?(D*)$c~QW2Si5t#tJDP4_uysW-Wu>Cwk6LYazqq#kH8LYg;vg8$ z7lXm@__%lf==j#rNf?G@50_trOreY`BXTY;+pKakGKt$b;RHf;@_pa%HT}8Ur4%aX40o2L5Qfz zxzi6-7Bdx5K;K$?FYbJ?X?D^;i5M80#F*(5C6o0+$C`nSG?R*7thn(rd@R>+TFY3f z7eCU`t60`DaJLj z3S#6Ml&YCdYO0#azY=4f>J%WB22mbM)vBwUj>gcrH&?nvnh$(`GN+vT_ipb!f9=XU z?|!hlzS?p1s_G7W@B44P6Zw8_xSsF1mFgmlqa?8%7c&T;){hchHL%LzWqCep*Xkol z?w_1qY@J(QTj>u54q&)x`oRKnyKCG1;gGRux7&#WALPNtS{JAwNs~siIrl=nQ3HLV z;A!pDN` zs#Ghq-$;6|*9*f$Rst$)kyxDT3dbNIfI$T|A;(;*Z_;>_)bZ~;P)wXxm?%dvOcdpAB zH`-DVBdr{rjF@KFI1?aBiBfACjq1IJhpk#jM>+ri5CBO;K~%bJQS_s53vwzlMub8+ zvxQ+9nq0SolkxV(`rMz|rhTyAyL_o_a{YwY9paYd9T_ty7|b*jKLT7f7K`+tH%Y<_ zlxray2w=cmH*2*{%K^6P4Fg;t9v6{AI!~TY0BBdct&_nRu$YD+FVuPs!UZ{>N(lra zD#tC72ogz@_J&hG2+nVBJ-B!05FV4B$1~-t!n?Ur#1>mQa0F2d1a$?k)h0ltg;8f@amPMK+ z{gq2!T5^)jUmZJNb(LmJvzZlI2vSEP&l>R7I0lTi`b(5#^~oQPCLKLf3Wa2JmQpW7 z%t0+t>Uw@q$g!}}CetoSPCx!*L<7w-8<5PN5}Dkc^i(xGBe)&9wW_)v1*Xrb0t&i* zsfu}ig2&Sv0PO7S+`hN#`#$93FiM8w*tIL5RP%+ucW~rlW6rbLe5RY$yYEqQf49@_ zthQcz`~B%6e)sl$aH!8*Jm>pC!ld!wv|g_R&`+n+Fv{!o8t@}9nd{h=WuBfMKiJ*f z>TaB7S#L7jUR|vc;KLr9JGLE0;dDCIbZtBt2SGqMTNv*H(#_{JDhN(&Y|>!blVmoV zrD?LczIL$pU_2VtYPGL_{p)}HNB`=*2b;}il_gTHrIhqS)u0k2$edFzgZ{{MEA_e! zFo*pv3?Nsj?N&|j`uHNePf>Lo(PHXhQ!!1W0+w3A5|UDREq@e`JutULTQR&@3?`mq zB)coCQ537Hw!go>?QGSX(#Z))*Oe;SoHm;2VA%H;F*FmXA2ST@%yXcE({BSnw{c^g z^d|!?+u2%27z63UmSn^^2Zz0#oh@$5O>863pySwiF-eNdG~`OHesp@eSj4^F;Kub! zr-!>zz5#v-5gCSn5>#X=(V-rws@AMK-a=}2JNNGHLvm@XHw~TY1N%b_aM?7Wl#qoE zl_)?SQm8PGVh-BIzz<}$Mw$q2@rr2`ajKh!r;+auaMNbD_2BTtgERq}E(i`zPO6P2 z6s1-Z`?y8CVU|U4IG_8gtCgC;j_30eExWkYFq`$8w+|{+C87P?k~M&UREKozRuEDy z=5u1)vxqYAN!Otw5B!0^Fk4VNl8R&3Tnl1I5QO7tG+TI70Rmn&gCZ1Ljx`!iY;DEV z@!^`T%9PbFK@p;=W-9i@OHB1VA1rU+`~ATL@2fbb3rd)gOLg_+1f zQ5BUZYGsKJJPMl~@s1#vI=LCNxT{t0B)4?oVj$O>Dc zk35P;sQi=fbLHoKI60_bkNq|K!TvphO_s_oPSh8czmyke|MbzrBRH$dw(H)wbl&QA zj(Uf|$q@jMY`B{1g3O}~AIh$orIT^InA7u+5`LsC$@9`Mr|gVd^7$N@lLHe^=+8>B zzpUIXMD~7Blp)FT@7$b=taxl!b}F*%NSZ+yw(RPa2*zC!P22=gS%_L<-WW^bsGNca z3DJUT+lu2R8g(k9jsY>G4Fm=XZHx$sBmii)`i7;!|UwE#)wyGx4AO68VyYo-} zGYvwYVMCx4O|o*n7_m~KkV;#9?qS7Y;3GdNej$kH7n*yC!$mMqUdEiOP~u{ixjbi` zxD;;bUkI{<41x>_EGXPST$GZPF5Tdq^%r3Wr~h-Vzi=8_5&eV8aAo=R^cBMWmZu42 zLNh31g;E5dikX74`Ve1dV3DSW(ZQZ17E{~R8cm3&dTV7+Z|K#k?p7Vca16b!YYqI= z)TW^|H#YzFxpV*8-}>$Gpnq`G`>XeEJv=^H#L)!G8g&KXCcf+}hw7<&h>R*(glN1A zdODK*??uildSO!vJ64G-%GgJ7|1V5t1xfIf?^y7*k{Nanvt}$rWQxfA7Nlg%Qa_SI32@zUqq!Tv!-n{uGN55cX;4Sm0pfUmFS6)EUQ*8s9 zX5@t_lYvu4?P8U|WZr0XhSP-?rK%EvT{${ES?#QJTJ?kD-o#rp9OKHRi<)IWc=&KM z8h-!n_pV<&Z|M5^<;(qke=(ocYtRJJy}hH2jTL`BJ32Z7s=j#s{B%0^;}G9s>&A34 zZPls?;9wZ?ES)0oe6?D2947)}3W#SK2|4s@*pz_yH4WbYao(EGy>_bwP>8R#O`}?^ zB49wUaBg$!)%WkU8rGMuZb&HzbjEvv;@;20IPDLHn_JuXwE297gS2gXZDZqL|EOB2 zP{*z`^*m}dkWy7I(^MM~!Ze&?SF2SW>~1=sB2TH|8R4V@P)#tC@l$Za5|;?#I5jN` z;gDfK-v}Gc_TJv{l`C62=T`6Cd$77z#V|u?hYYf@wlN${u(+Q;&yEiJmoBUlzetgW z(WqCFB&*aaDfiY~EEcLkFOG-dcof&0rmQ8VE~jy%YPPJ~p|=Q9s_CoOyNZ^mdcES> z=JxvG{^_O5U0F%E>AYy}tH*NYM-iS)y=f)0Fy4x$Mya1rXS4#b0s^^(=W&3Q&%MYr ztc8y~wNNO_g-Qbv69OGv*k12J=#Zcza&U${EI}aWq>_mPt~Q&H4P1!(R`t%qy_1vo zU%q|~hy(9UGO|8FyyIewZ1wsxJGx~8AG~+(4hFVSZ-%M#!NZ=RE8TVti%61CT#?CY z7?H~uqB8PfFGP`I;o26)YCM@*&?OqK1=U2-n9__0eaT@m#fdbUF5-gKP6yQOY%&H| z<0eiBAM}9Q6qwx9F+`YT*$km3HF|5ddMAUyxPRr`hM`am0IoztzC*lOlcHiws7?~F zPh&ihn)hiSt91s*TB}>Ot&xbD%5hHaD}br0ltY&QRK->B;!job_sEmx;~$Ujry>)Z zDm&?Kg2VlUWDI*ed*z4k(el~B2&AQa+;OG6rS8hg%KF7~hHHDH!SLwNAGm4&Wcs7yN*-AVyLS%O+`r1s?;vu zXut5njVuVKv)TQl-d^wIcrbW-_i!+slA;1tj!91&4Vm+%gt@2>=0TP!DcL|7zdEmf z!A(0+3_ey>*xB3>`a$sv1Ek0;nXY`sSqVxmGh5bJUlh5;Q=#O9?**ULL!l@ZU#MvZ z*<1}OUv(2_;;R`sW@EoisIN{+JKL`w5>L-$LOdk%Zk3sBZDP!=xg&9%mM8FhBI6#Y zJJ@T&b4#|uML78Wv@KWJCX4hyqH?@~{i-p*Q{4z`I0B720vOeyrK%uJEgekU zH1g(CMrOX9X3fh$mP_qMeFDBZSWwnGS>K)3Y+&?ie=^@aI0DTX#mS|M7y75CzUM(< zj^hY)xLc``k4h4&eEaUrr-rI+bXHDIdJ*O#=hSUbq>3_)=D_j5my^k)Ud0Np;uF#& zQ9+dhhAtMsQBj;iAEs37JOgz-8-)gbH%<5V54N_?XQzYrKG<8WYv;Dk$6-VuLsh49 z4@5h@=;*kIumHz-{`~pTcpUgar_&AszgDfplOSn4zmcXwy!FKh$oLwg}vUM;R(J@?-G?`cNYc8n;YH1*lo^MYhL z1L1vY7&=hX$;tR+JXMtSYnQg)f9KA^4@^Vj4wTfMmeOM+dFIFCMYMP5C3zBp@=l2X z#MvZau<%17Xqb#4!0}aU?qcTejmNc`Wm2_6ytUR|cmX1`Znul!7>>r20I!hU4dr{v z3JxqzGYG&2S?CZs(5^Q}AYfzy@)?gq@-*3A>jsnASdltSTgkHb?u`n~zOd6BjOYFF zf@w4sGY>#8z$Za50D?9R-NhAOMCA0OX@J#)s#U3Kn+as1*{o3oy_5|n^LcF_YquP7F(nlu^?Za0K%R3lf5}5lb*ZX4mvE|q=TQl)7-2vUCmHHrN@G%x@F5CBO;K~((sc}FQpfOv^5;l2R& zbn7wkgeUh4o>9urn0l@dWYir%t~OY`saURTn$&QWIIn`^Y4YBb$}FHv1Qiqpl-Ytp zN?00Sav)RM2~&$VUe8H|W;&lA_D=8a?)~0dZ|@!VN%z!MQ>^As z!-I0g2yErT5Qz%bQaM-6o<04R*ZfLVSyjR!luP#`c9zohi}DZ!xQv@;32l-&h1yc$ z<5(*w`I4puHL#ODqX$9#j0#ird_SIkf7*PdF|~(Trf7vuxh@!FEtB$Jg#a|!$h%Gw zD-QSPbtSnW<&zBB9km2jK3-w|*w2!Q*NO`jFSNxJGD59f;a&-tW>MmzcsZ@&KT{=*$Z$-PKEe0Z?2zR~WkJlx%FwOX}W)zHEJDLM`~ ziC0#;w?25^Hceo6PS~YUlml*p$>t@ev;qU{3dT$4EremmFPhDchFb#o1cIBUKqR?*^+K)D2_p{#Ws;}Z zf-OV8cyZ^0dq;6BxsJ`GRH05?Stk3JuWTG03`Zl0gYdY~aDioY8N!H)lhbt)NgkXW zoeXB1>&41?3yMp@+=V~m`6qz+TF%t>Z|@gcao9Wg;+MbVjs0%7N|`W;deX}~c7;(l zfGqTVQ07s#nCR+MS8z^~=@h~I@o?%dW)NO1&+nZM*4Ec~a&g*pEloiz#DbZ(v9*yP zkjPVN#+)VO#YrhUR3wpPY&e~%x`|I)X?1dmESrK5>$DblvxAdU;2caQa#hd#wY3!~ z!?L1d4oqfKTx-H*ibjnP(zGBF7V1!Ho0@M@sI_-=YFTD10Va7l&#r4_Pypz!amXYG>X51$t1I0&r|J&q1dw#Rg@?Gvact3X3E=@I zPuX#dWCmHJs8IhoMXkF=#a1AzL`jkG#$!yTsf_?=;CV0Ic=otAJ?uY7(@gr5`uC&7 zH2W~AFFzuHA3fM(w4waTDhM;_hxc-PY$-p3l+s6E;=G@%xE1Gx7jI~t&hg%Z;PzhX z5ul--Rsh>k2z@OLJfi>S^OP5A!r6(MnIpk>2tICxT{*>pBkJuT;E&YZ>jy;ds5nb@S zW8GAgimrio^sopp#XjuK`vWFZKwOr4E-!JMW!Jy_`Ob6CYFt`Be*c}QcPgnmB{VW2 zwu$5-g5J1Qc~TRV7KpfqBPhZ06J%7EsC*d+6lFCyE2F0JstA`;5eEp%RQ_gmrUnR9 zDNA7zGe3DOr3b_hIDkmJ`%wy5QN|w}u#?!8p9v4kuE3(MSSqWKA599G#{hY;Oy zgW>LM#8~i$ClbO!VR1y{gGzlH5G9ful0#Yzrbdt?-asA>g5BNuo39~8*IR9UZNpjH zYIIkcwJL%EN7r0Usj6yKQyY$Z?Rx7AFW&ff{{Fu2(D0U zF4~j*XsA<2@lOH0$hq)R5gB4MR2hTTlNgzipPGv|!rE8dw26O-_*IlkJ&yxt=@;Pv zN|+a`D5CHib30PP+D>hFJfbq^9GCb^l=>_lOyY&B6bvhg7Tr%`lesU}C4)g%%2nJ< zDfOzBf5kM2a_~xxQQIu8+NuIoA^(Oa)nF)KFLSn7gf>=h!(+&2C;^@N<_;@ zo_{ZDeASJ+K`bTeQ*D9sGkQF|L!eQeqo|RU>-G9zF!Vg%)|J8OG4{1MB5t+2Qa{++ zt*W4-+4@Q|n&*~VO(*lC{=_!rwRXKwZSrYJv+sWQ`<-@6A!u%NI~}K5oz1l_-ER9{Xqk@ZdqnK1ifMy04)7I*Aqak;y|h@Sx(dEu44pxNM}pvC zG#c5q4X{zIH*iNh*xkE-Z-0Ahr`=frKyNhaz8AIH&a>BFy8Xf4jpw&i-H3C~woFx3 zk|ccn^_$oTzVyW#*isGEpiB})>UP)u;17Q9*S_|pwbiyn1*Q-v5TmQETL*aEI9H#{ zhgC~;R1yuqwCf;Nl4N7Wxp?IQOSC`z({KH)zy0#HCwS+5oS2k)8Y8$-XRbMS>s4j@9^L^e*GIQvv50=)mA-BlWwQRWZln{G%pN8 z8;+;AK)3GPJGar+Z#cS$sh4heaV8^6=(UbuKpuh&L{)5-V0osNdMfe;;0 zPEQi&VL*oZaU`BlO)ahl5yD(8&bg8QvT}df>2{g>lNU?8%ro`z8=B4>ixT?lZOL{( zU=ySzWDm_hmP+VPcp4HIrR8%4KzRI%lW`Hl*^+KYa_ zKt&|?E#guDDG!}V(9Isho6Do&Aw&7Q^yaLLJj#i7cEpAB$h5Rr)`qiXj9Q2$WtOd4 zb+)=ev7hf{keq!lkAor%WRD!caB*=nDW*wUE-o~+2;*M>p{PGd21EbmdsulN)az!e z9%VeX4{| z$sSn#P&6gZf=Dq$7t#|y9>1?X=2l|mr4!eK`XO?szvt1o^EO z>b~V6I8dl4Ih93SPlS!Xh%0MxaG2h3@LjjgGRlDMGRn$^m9e^7K@#8J9m!u=e`f>I`yN9 zZ8cq|Kbe8-2V9tW0jFBCMVRTToF=DhD{X%^ZnYW^9}W+XR=YKTImfB&9vpO5JB@l} zI7&}O{<)ovzk2=s^BXHKzj)POBw>_oY_vfir%AY&2f%{>?f`J7z0<0TJwiX34XcnT zWENo~EON(jtcn9f2*I6*VMc9R8F}Dj1#kYtyG@bV@MMY|<V6oehUw=Z;aR$Ku5);F_~kF&xbt9tFj$=1 zYRkOVR#C&%mh}(*{%=DeS$H8ZgYQR>M=FlFy|tpLfJ{C!Ro@S*?TR-W0l`ZVHgXD? zF0Iw>-WzFl?cRgI7ryv{md7~M{h|NX&39{#Whm^q=Pt%cw6oRp;>ecclUbaO~KC*@~hT^B5F(Ana3ROo%+FDspqDGpAl)6sp--c0A5ref{Qd|K`iFhrt0Z zhbm%{dm^GP@b9W&F{N9fvfv-6oDu zO8hJd1Yl7Ool+ANvW=z0jUt6?f{+zzlP>2@yB|~sw}0V zX9N!j8hM{Ds20?z35y%HA8G)c&jLURh9C^`z)ypKI;3%X z{ybmy?c!NHOd$Od`U`@;SQg`!=AHOJN*8H%m0Cl_oom`Wr--Km5!_EGQHMi+o>!An z{tWl(DK!MqoOa3d^V1**Ci1#O_KxTzUI<($2A6_DJ@*%>@26piPcmiMRTP5~OQZ%w zwDT8Fn{UmJkGR_&hi&{95fQ%fr7zYYeJDl4)YAeZ;j3;1p>|pIoAI7olz*R)-S85I zi>QhY73&D@#-yyMT_)!6@{?u3c9~4hgc(l3({BV?&PC$S*%d1>H2(#Da#|Aj#dMKB zRzHB-7aFU$x_KEBN@syZnf))sfm~=E0?;sffad@J5CBO;K~z3s)Y+rFqeo$5@kqER z^PR{`Tp|`qs@2t6qpT%@W*9|M;9<0L-%Ak~@bWL20tO&tn;NAdHlUDzTO#>!FuCvD zzSTDk#j%ZQ&1^KBX1CGlt~Q%p+v=J|jh78H*4F>ct!--R7)HI(_;4`1v%CMtx9{KG zJD_+{r(WuhTZ1G}vkpC=t;QT^8p56cekOqhs zn;0d%n1)+%r?Um1WJZmTc!3;b&^S?Kh0Hh8RMYebe^oSu2Wb{G>ea=A;5 zGz^X$fQ%KkEC&_vz0Kyy$qDC!3_c5pO|RE$lffXVRa=cFVgp`TOGz~nhbEj}l_)0R zPogB2u}dL1<$17KoI)m9I|y~PsMKt45dm=PN?xm|YwL|%PVVl`F-g|it*^ZFoS}h(4~DbF`s&Ky zutwux7G~H?DbC< zUR15t2cv1LVar8CSB7eK6+}>=jJSK67%#f5kj7?2*D^7wdln`7ab6Dy(pqMfczuan6F*uM>S~VAO6K0o` z>%-w3k%R-urii+&>cF3C%9FESOVs}Ou2pWak74_>oRAOya`BPBF^N6GX6z$xc!GT~ zC4D1%>DrZNzxtJvDB8XIL40@^jiyo(@qf<=E{9=;KpfIr6jE{vLME~IaOR&0&&?9? zXT_uEWde+-0jBUEiJ4(3m#=GU+mt`V_MXNI5cwQucc+M(-NAYJi%_AR2=qL(cCAAr0(#qDR@#Qc4 z{(trV&FL33o^XzEKD%=5nG5UPIdn6H)l_BMwjqzIj)O0u+cu;Dp|)_5Fsv0vR>oB% ziB>DQX-VN!Dlv9>Z3>ZH!L5Ck?S6&F4-?r?OSoS0oV+tikv}F5l_P-NIPj$DNFes2 zj0LHIHt!7h(XwOV3zYGC3W`3+@2JdAr=%>o1iq{cFpA>CS!o3&LD5SPVJQMGN;TnZ zQX&`txG!9MM2Ad7@cjrEr=xs2k%y-lprhqbeXwI=SyUZYa~;)oWt}z`%&~Ml3HA=X z2My=ECLU30P2>-x^t=JS`%{~ND-=|7YhpOq;)=g|{l7St75V&Hhx;cM%lhH|LV-TF;tL zl%$w!?)d~Q@3P_-Xe57{&xt94hQHmI?9H{Dy5gkLry!^Jho!ir!c^%N`+ArKb3QNn)R`#Hvquy1L6}Kjmq;|{0WdxmZab^)q+KS5Y1+D z0Bh{ppg*xDhH>IWaidlNTQwU`>Wx~XQ5V=O1ON=9TCEP;=J_F**K9V|71^@PDDa~w zG))7m`)n}C%xYK`Z`vH(DzIW6tRwV3gDO#UH}=#YkCR!sk?gzm#(1W89-MN)%Pkj^Wnpud!td)!ENCeyx}l0Eq6Be=z15)Xfa#6u^lB- zTzjz^V!QR@c<*5Uxl8BdGy>$cELY|@nhagQMXKCaIipY>&|&ZJWHcPEZ>&O^P+U_4 zhok9)Bt(fC3z@34wzB%_JMUQ*&Zc$w9E1_Yi{t)0m@Itn{=>@`woS{zWXpi4vRLLm z--P}-Elefgsz?^)3GA{QhKXb#2r&95)G1NSHm>39UZW0 zfA`R|E5ItWK}`zBaJ5GU=by*fpx^LR17B1e6K&A zv>Lka&j3hk)ha}Kgovi1)opXOD8ejOWwqe;@6egS;I`}T+Dc?8NzT1xvNRK=6qHGRb}*U9$S>`EmVCd! z{#2n&UYA&-aMy2L)T#}#^2AU?F)t3L$e)A$tW3}^KaB_ji4z#|vb1-Jd*u@PbFavP z<9<%{Q}5@TcZ((`f1W2UmzfM4L#tW_qQqOo(@Erc_#{J-4Mil7NSHTY|LRv)ZR=nD z)BpYmK_)LMZdmrSo9il&eiW0mm!?~~a{aj*Z}$fVHRVa@NO-0Of`I3oDHbG9 zAc}@@ykIFM-%jUhp_*5i`L`B=^DpC34(pdm?)=}EPZS>7q!PmWRN_?- zA8A@F=viVEd07H840&apbvGztkAlqivS5+UXG$2Uvw4gg%|bljD2@Yhz}ntoTK4lYp%QQ*j>x0Yg$E99n1NfUwG-azwpu`2ygA~ z|M>^Ezjy2Y@yY4m|BY{St51BN7D9><@tPR0kNqieD=XZ_iJ9j$sWJM_q^mZJ^G=}o5?pV*Qx^}(Ib?sZaC>eUF6N1c1 z1OSm~DjB)lQ06^$CNO;Sg%9o=nl@ni+?yFzWj0$}Jil{z*!TQkb#1lX?%*oy?;nPt zukzkWK#Bd~&;kn#TGY^W)4(SI!Edi`7`8F-=2gd?j3xkVV2VM|k{P3>=4s58PzD^Q zDjfMyPL2(DZJH*oF&J+gUz{Y*K6CMd+c!s*>gw9Mx0o)x>F$FE)oK;sy$w`et4OL$ zEqbxdq!HqN;E*tmQfk=)XlUd3!YBfC2ZLT~RLA47ZJStqOj9RIAF3{ufRR%wik84A z*PETU-g>XqYOCDxAG5AjueVxN1Oeoogpo0;5AHu`HqRS|{l**beEtj18J3j{=iSxr z?VImaD^|s=+`D(LyV8!L_@qB>HH;Lek>)lZ#;Q&I zq%;UGF?ds;6yPavVG0*XKx-lv0!+Ps{{c=Hf(_XqvxrxE+KdW&ezS@PomV*L6bo#iE|~5r-rU! z^Cc@a-dznGJa7mx1i0=-s9DXL`uz3tYuh`9>b(Ew9U*Ud+UdvpK$z0RAcI}zg=4NIYyGI9;hY#X@KaV3R3aQ!_0f6Y46(d%T@|dE= zC9D@DbW$wMJo*0#6+j@_@>>YjBL;1@-N9}4-@PNJu@)z07F$_d zm#zQP`s%I8+?ma2flE?;MvsUrnotVSX;j7_I+tZhQSQiMfh@6p&Ixs1j9ZrBN=whk zv#V9+0G0jQM4p6*wy1&+3nnOb$@e66nN76(P2plBSbtuvz?K3N%a?72ixTs5#Lnxs zgI*`RnMuM^OjoH@wRW58J~0unZbm*fq~6hSPhsy?DsH>eX|>imor}%Zxr(!4 zn=M_3GGJ-im#$v^($z~|67Pa1kW~@{p7aJOr;PYnET_-eD`!6`L=px`w2JjI@7se_Jg1Ei|~^?!4efq%#F1G01yC4L_t)#S6xaLshk&T zp*_0Qi{i8W#z9ZukG#A_gQF~{?>`$;#gQbd(dZ7anQ z_3og5I_o;~BtLoAUrbX4k|xvHA_L!QrnjS5NsTXPftK*xVCtyI-!DD(ll_o;4?einXw-(2 z>Cs8Q=28ZHtJRnRKTT(#{B6?)_X40`R$M>y0pE4gOg(?@Egai2G!^`>Z9Bv9K$dlI z*psPOuUbF}+?fLexp2~;r4uxqV>_UhnXZDy#T)|R?|I&h=bnH4&9|_LwAyt!U>7c5 z+}qpp7HqMY*J{<_Wbp^z{HwqJ_kMGwZ5QM@0Ww{u`oNSvl@eOG@!xnnYIW+Gu2~ND z6_I5Gsd*NM37!+l9|pRi*QkEEM!=UoN8Q+5CBh;jw~UMmO36ucYjb1RAFZ!Dw{%L=6V>QW$@j8ET<6GZ;v)yW+JHK}8=AF@C_~kD@_tmdI3$Chn zJlx&eU+uQrU0%wF;mZgTDVoB28VH{czS9y+KuC}^n$4RxZ@&2a^VnEzMK1HCS>#w1 zS;};+vbTS9ezUE~tYRCNE?oG_@4m6p?(XmHrm2qr@BYJs2y#rJL7M|_+iqIHbS`Pc z`Az+#({Kuoq!<=!VW`4U5rmvHXM-bzE_USkl+I)sg3(Rl6bKuEO_Jd9nK=uJJl2Zz z`o)bn^iJma-tKV)Axpio@a9Dpfk2(l=hOMZHZ=UAida$A2$bnIIY1Q*vyL2M^2l*3 zQIhsf2i_utuv4$Qz%FEinvhy$7|;bWOd$v(IgNwV8_soA>2y1ox?~zdRnK{kf^ zsha@S*=kD zP(tO}JW~Y3lFFmyXD)4i?xpKdI+3#R_L}khjZ5p-zMLiCofap<{^U-OVY;MYyVV>{ zM$(f$iXV9LGk+vmfsy*^i!W?nzdn=M(ecsj<{RPBVLYAZ+&WfD5D8_JL7tIQYms4; zD1=+~@nRh0XHx0pxg`5bkX~|kEK8|f(B?S>)URm?`wn16n*EtT|0z%FC-;;*6KOxG zNciEU$do$Iin0#?$CI)o8dflFHAA3Zblw%{p8|kfUgA#y+JrBtFtVill2nhP|Db98 z&h|MhN%i4v^DiG5fUdYba|%M3NE70DIN#7xsbvzroAip4kP56wYfq_PSeCq!rTZ2T9#@%itQ?nrJA-lG_sgHrw@VU-GGuF+ zLqM6nJ#`DKxR?vO-!sW!c@=(fM)jxhJSRIlUtOqOEjyuGNd?(ge45Y9s3V)IR{T`X{MXWIW~|0Mm3ELsX(s+GOewwoSYoymZfMK z7-hWLJ3g+|svBF|_wVn@lsE(q%LA^;h$;?2tpm`X9!_uH->cOxYT%d?lBF2Q_*jUj zIgzL}Gz~KdgLcXwLZ@dng$-aK=+{JU7*l2dPw{b{~MhmCuR4O~?cmDLxzR%3c z*2VKzE}sJbbTmRd}L78iJvw{C1U7{zUeZM^hR)}5_$oqE$+?^eO3FG7SG@hnNNZmkYady1qtTnk4V0Ol)tCTl0-1>R%o1{b4Z zJ|~^^mQ$}cFohsX_+qRsA7mcQK%Q9*;n^t;I4lH*peQl+6IvU6Lgj~fh zK#QQ#K1IQ--dt@LX{?evUGC^qepqp=JdG;0sc@78u_X=?qf*5Mbxn;@tZ`xxVG3TQ znd_8DM#)zR0h5fI9HkfpgSSuzM$D@fb8UNLXRy&fTBvHqEKM?7*om6!t1D}(-}>$Y zgieTpldx);n)Gx&meIvB0vE5*W{KPR;>Oy`U;aW;seAy||K8o?%>Qam~r&a`r8ADKWu<0y+K#Y<%+AfYdoaXiZ-5!zEqN6fq^ zE&4MSg|}|r`pUtrFMj1Knp5Qs2LwaU0gu9jQ4@cziX;#^cdi*0BGCo!Orf_&=Y#P7 zGP(ELjjjLf!rn*7&5u(S?l4(aex``|>7RI$GM)gEQVLCpyOUyB)L6**^3t*8X$!g@ z?-0jSJl5eM<^DgiK=#y?N%U+j&VFAKLB=vp5ZqkO1pmNaB$p5o9^}#)CNIxfAb8=9 z%WDNBY5Bi0GT`H#V@;9N9BWK2u8YXRT3WC23T)A7y0pJ3FpEKDi= zER-N=`Z)tgP{!g=a?IG;!TlV~*SO{YhS^5+$IrM>#>O82=& z{c^ox%IX}OL6Og&Tmi_1&>}REG72gOOU%bd&-uLL&-tRw0|0DcvP!{~f*?4S&BUF( zHOX{IKF=hb#21N)d#bD=;NSG9C&Sb7oKL2+S8#4}hOD7W?fHZQdTqG$L&eSoNo}^u^DSWu$IgboFse9S0pR@APtzWzNlmd;(hWHe$-v0 zDAmiK@Lc4lI0|@r1AuO=vH-fmYPguy8&yX&fQAN>Sq`idM^rGbFwghEn1Zv18W6@= zp(^tz!hNeO{8qE+Ej(Q|=8Ir5omHw;O88Nv!DK=3Ur{tw0d9)?kXAF*aH};01c(X@ zRql^Qfgjk0fiSYA^6&KF+VJ3e~<{oCg+Y+H7<*=_@{s+#rY`+GY(tFW<2 z`KRBx@%%Ht{Wm|KXH-yl?U|LutbuWz&1ag~kct5N0n`RW4sisQns8A@HOgc&4q{5A z1K`Ql&dzb~*l=ux9McO~w{={hLakRM%h9Q;u{S*Eovf{_zI*fL*vqPwRviZ)Giv&s zv5T9lZ@&FzwYdTUUN;mqr@_I(iSK%N;z1b*d}?uUuzz&@`lW;8LATXld9;`gZ#?%L z;zQTfizL>hnydzrmPX`W4v{ZC=}k_C!TLFEH1UIJxVhO<5Ox&lR+oOGs8$D(fBT^4 zI`tq(W}cVHQkdteln;76*=$8osHw>$A%}rDo6Cyv=FNLAz4&amf*rjQdckb72j$xc=)Z~li zK$s_!#Y_(3T&G7G|xIq_z|dqC(A4| z(*@Y#dBz3R19E(uiv$^o+u4-;ga!C#WwYTzxRR2{0PdLO|M`Q{hoe9E`WL?d8IS}7 zs{iJQmgXTxFS%%Cu!Q3Baxe>msl-*7rsTMJ-gre~|Mber|667M?r zJkLfLo|XqEk-<<=lt!;4D`aHGFOa4fOIDn9PhJKCnY3*G%fwqbL?>Hbp6*L!+(Kty zkIw=jG?W*T&xoS@0Eu7DGV%~4oG3PsL|-9M<-5#DD>aM9Cl^0fd}&Gxehlr5+{1ZB zRw0TpWImXp5`!yjB9IZLMI10Z{)EO=2o(I&bCInSOEPvUDTqL5sq+PB5*^BOwT>^< z>kXq?aof%ADmc$P34-Z(6neAC!QP)7AO4YT{e$P8`)Ze(flT5Af21dTPy!nH9a6ce zo*R&d9-TguKJ;8#4h;`*xOk9DDla<6Z?hD@K}`)eY%JcKHH$`e#g~dOS3w79YHnZy zrT~u6#fQfCsd@hBl}OIDoH}2~wIX*D78+@sMQRFwpvS3p$tgN%`2C3*R|`=O#C0m( zQCs?XC*|7x)QwN<&3#F%x`^0r{o&wG@9&d(E>d^)YrN}G96 zPl{tT$2OTSg|8PBrhKGCpKD z{3*{R|782z&*ky-1^}jI@9*tux}{ngxMwe1XuLTkP%_pb%d!>=A8|7^*2Sut05_8F z4JF#jvgdh$@9UbL%ZhGUY3L7!6UWwEGSV~)YMT|02*(uw01yC4L_t(rh6BbFVmUhTU~85YJ9VU>fi)nuU>Hx1~eM= z<6dvdZG2kohN|jvnx565K)$Fb@|Y66=iA#`)CQMYL8TSf-rC->%*v?WACCvkTI=2i zM~%Am5B`l`|JGlA`wxEK|J+M2)M|R8whDm+5aErtj_=+%eCEm}ERWl*#$+^FX*H>naULk-Z4*eD+-+6geeVHiepa}Xp$9Fc)2MXY zl?M+WNQxDB@%_Dnjm;K9y57kcs>^U>DKa}cJjL(V+x6*ak|yz$E0+-jG-^(@;_U7o zZmzF7CN#eDrzfYCYF$>rf2Ua<$vS}{g(zQ$JngzuABpc|MNHRG+J%o z?Q^SLQ;`9nA50g`daL5-QXP_DEesJ*swbyM&s_f^o*kc_MsZ?SaFu&HD?IPCT7gnA zt(Z+I^?Jug)mqgs4ag28pXa%b%|*bPEO#yYbTCBt$H?wej-zle@#?k)#19yN0M*dd z&GnVZY%!TEYE>ulJRtl#x9&o^saGqu)J?r5@* zp7inK1Ary!`+>&~>3Af3MIT?w%axpQUw@!$?7}Zxy|8iRncTH|gVFf*w}Yd@crwc8 ziwrVQ6ylyN$mTQ^6}QFmFmnMmdEZ^BD6`Hv;LMBCBA-Kc{Gwac+pDbEVwQ_|kI*F9 zgF-gU0r3%dl|UaqQvv#+FmO{aaskTN617Xq{~kYWQJO|O6Pe%fv;*n`MP1$`(T_4c-u{d4{;hibm2Z4etv5vdt~gmf2psSUT~bxc zj3ZTJnqHW;Y+90ONUBcCfTr%K%KzbO`QI2!|God@zkr5G$?(Dg?d*yRg=EQcWr7gr zXfZ7VG;aGNninsTf60*ZSb>#K%VhLrZ!Zx=5XegN;^QXFqnW`ic$873^JoZ#=pbPz8g0-@`?u=2YyRH6mb=FFB9J32>ni>O!J}ebHRR$aJumrV zx45U{@$?1&K@jqM8}haXt~6w_Tr>=2I-S+rs-bCt!pKEYBPyB~(NWH3oRZN3m8epZ zB)}%nIzk4W(zUZ};fJnmkoR{GCO7MhHXPm-R)ig~k%Ot5&Re=bDAllwo59SNV1?SJ78@&1Ud^*|LSpCkMw_QuV zc=7zvQNP}>CKG>cZGGWQtT>)5{FC9deW{9V07sE!i9*Ic67>jUtasAGUV;PbcH4*h z`&HL^{@G`Gz0=7kJsq4HroD6SLdHuy`-47UVx!&wA{mYboo^&o=;oaPak+cULVbF*}WK%Ov; zifad0tYhF2lGVY8H+9FsJ(&FFH(rJ~mqc-^{_OtoaMwSfPm7cFwe|bEy^3w#d+@N) zXqk2e*so?gc@XFom*t_Q7E0BM6aTqqF2w$nrJ5i3mhB8D^XXo%R(Dsr6`8z2b8_x0 z^6_l3y4t}csn-y%#hsO=N3boPPG-jT=E-oT$Qn+c0JJ1Kmb;kGT$3z1)n;Qd8J=Hj zMXq`}$-tevu8p|v{LThIUvDrx?2l_u^EI_rt7mZtg&zmaTs_OAzIG=gv zHro(J07f-g1}4Q=VY*|zR5fbbgv&kigRbNHappz2qi1_Z{psm(r`EM}>;7RMfDO_F z(C2IKd~koiud1dOMSdI=WNl(ePxxTPk6q!ObP1Gy@D~M&gc_g5@iQ0Czw*jg{fc{g z?_l=E8`1u5G#qBWpT|L-kZ*kkew0%DlOmTMQJSwo88 zo{Wn9hb2remd8`3P>Bp4AQt(IZeOB##t)YX>mE)em#1GW{W(jlFD?${>4?!0-Gz%p z5TSAj+sg|nf+is+uw~ZK@)GAl!Dpq8A|e%q@y2>0awtT^C2_5PIXUP9Nq@l$DF$}2 z;Hf)=Z%N0XY(oP28G(O=M*@t%2`!{bSsN`=8KEp91YWsOPz62@3`jr_p@1SXhWLwx zhzv*;pm~&~VdRa+#l3rawbH)&OtQ11w40VJHw@NI>}Hw9#m>r2Kk;*xQ?D3+Bg$ln!1ff=znr(e*48(rh+(aZS*{j}xO0oM zk~#4_f9(0s<@JxeqU3U_@|#vo)D>g?UaYFJ%8WFM3pw+rz5!MB8eRJMd#UEtA~}-e zM3=0zh(F!v{or#uuS6%&^tE}rT8*8c&?Ir|KP4mf^Lt$ZSD48$*hRUcgbx!tH8S?u zjCsN1|4bAQW~$-2!`Bvd%hJzT6E@Hl9be4LL8PDjygs!+{xf(yy#c_2tm|!3;B{j* z@brpoSuQTWAI^X{v62DxMo~r$Vr51)8(B`aC$h{N1z^$E$e5D4w8pxo*_O%6NmI|y zY84lF8if0FK>>nH)pts-Kj^lbs-hG0XJq;bFs~ctcrtcX?6wl+|L|g5vXP=@Ou+w^ zw^$g86$Ac9WS6nNvXW$oQuMWkyL)h~+m+vZ<%P*2I6NApY1lgs9iTSTG7Pmp9J-dZ ze>m92TkUE>e)rrSggm+_LCJAkaJqgFv|0_s265o)x@JI_Ab(Q%=;%;Ub>ELd|LD@i z3plT4tLeBdc=>{wLlI)ZLWVs*qhF47OE)a5)@Y@aZ=du}jupd^a^vRvxABGDm9Aq~ zOjD~=HzpHmkgQwUjpr{m+xE>{yVJjzeC?}WG%PY3{PLGy{^LLY)|bEVG6;Sw^dc`7 zmNQ1>}yw<51s@$quielpG4EraVlx(hd6@Ia?%fu9f=!K+% ze5HZY)ZP6f-Kv4Yd$`{_?)9I$e%>%ZN2!UI`GE(p>l1)(#F2D#+}m00jOSq<>Z+<7 z?GL&uYjH-sVaXU+&Opl}Ry1)+DmxuaE^M}HRr~J2pxJ0+&!5a@?Pf*EvPR8)uy=2* zvx-Z`1snorxKO$1%q#H%4?+c*>&o&h0EksFL8%c~jwz(+npvwAWFL|mI?Y?Kdl4_} zM*NHCn0Sln)Ugcm;K-SuvmiKYnU7`&J%DD+2&anU&!3pD_#qU@kNk}% z{RPf9JEnbkedXB~o|CGLyQA^(2k*suyYb*Op3O4PBj}H*k2p#=Z%`Q;GzQyfo6Y5yQS9TwzYMalXpWd@eryV!?-(Q>dg@EVe2QTrTs z=X_=BqJHk&?9Q!Z{~^g8tQ052(TVwj(a|J9rSoFpLguZ)L;(L{|m|{85OCyhEdXXr*l*+%zkG?ct_OC1B z2~iPD%>?3*vXRJUSaAH56u?bdOC5m!RD?Sdk%-V#d4#}#Cl#*2(d+yTGArLEtX)MV zGU5crQv`uM_{WNQA#;cx&*ul<`|iWgEqE6emC0&t;>^D7J73d_HAoC9H>=Ir zqns~E-11Mu^B;JnjHDTh^|~FXftJ$NNu5<7Vo#XVkHe}bZCmEp2;TRVNQbl+$UZ_G z=~Io~4?d6N7?(mhH3r|BRlaEE)odBhK6Rt_vwThqNJ;<=tE$pA5x4*5+yw zglUpSVTe@{$W|o|Ug-NNNYMUjp1RjcEEW7}71o{9qAItNIm*Rmt(hb$~Jlkr_7qi)T zf$svL21rnKD~6#>XOp*He`9l{9?9-^Uw{ACU%DjKw0n0S=2G;PFa73w@4tC;1i{p} z{`|8K?*XY!p(|8Kyue8TWh&g;gZc*{VwuHB7=S(_)zp&!01yC4L_t)xWJOETqEd5O zRL30d?Ce~;c=6%hF^1#XwQKLc{~jcZX0u5tNt)()3+j){`$LILUF=6dl(J^L`uaPU zE}q}n+U#^z_Kv5w-+v!*&NEk^xqtr-zPVDh8qIpUUG1Hm4o1DT)wNcq`+`|JIqLsc z|J6VJJAe0gx~nbA1?{6FZ202mZX}_HO)aGa8Ey?iHU=QjwZr3+;b;^@`R2~0@4WWv z;PkZBS^c9w_`{XXC8jYy^xN&cKbRcs9pSm01<&8OW;(WN$hxVnw;CXo(qeUZa#SQ) zx3xAJ&DPeN*t|fEgQxGd>tvY2B%PyY-W!2VQd3P_4$J{y^^J{<{losniwHZ^*)-WZ zI9*+JU8_2r4oocL{n53n8;AroHQQd>KJFi>rgQ1?B}0*)zji5(g8eLh=bgLjYaLx* z!PqU5oN2OdIh{4LKlM9}daGIM&lU~WI5;|8TV1W%G|n~Kp8H<4R-23`&02-RG)W56 zfQlyp^HPWwfY%YFQX>>RJh{5vfk-u;O&zVUZ396mn0tlSrejoHkoQ3t`99Te*jAio ztL>&lVS@}v6cq{%3b+P>gANso8m`H4Kus@5bxE~A%QOJwDUafS`11V8yCHs*;wXOH`=!!g;}a~5Tw=;?OTTgT%7sgp z5xyUe$Njfn4-WUklaq8d&HP15x&EA+7bJbT^e%I}xmV4ZqkoxKvs|5V>yoEuy`|Ke zO0&ZZlU^0jpRguT3)D}z%EUozY%fb_gm9KO4px~N-W-#H;Uyd9lKGDcOnjX?w z#Rg6_J*zz^W1HeE=bsLfrv?!c=hB;Mo z{M*0vH_~UH4KuE172-k{$&)Jpu^P*nkpx&_D7(2}?8DRblRhVkjfpEI^BNZ{9lA2p zCa=z_pRcesV_KY*m!#xDpW2@ZMj}J}MkVablJ#{pKOmS%b^J`y0yFOy(s?Td5_ne0 z^wfK8VP{r?J*rSX`H21K=b4gVPty}4K9DA|*LtCu)&s27PygusRDW{YMJD=uvWl$L zwKRZ8u74(DPJkuL6x^y*eU*#O>heSzzwR~FD%*tU5Ym3Cd&<-| ztUn{mzRAo3i{GqQ>h-GM8v$fXnr_?nVgZR&s#dE(;Dt%C@F4=`U}VWLO4SxopkX&c zq^PM$oX=--$1%2d)st3o%eqIm6w{W#_>^a z;g7wA-|4g(ZUcMEbTJ;C96x_;$0XksD8`_BIrE_E+;tT2PM*#HmshZ}os33ytwwWI zQPMC3R@+=#OTy^Rom-Am+1}nUKp4|?fI#)&`1I|YgUeU1 zMF<88_VD3uy;Zw-VY^;;yDKjiJVORb00r_SQ~&cLkm>73&hIgbNuj^nvEn)o{6Cn&9nw7Y*`ne5WJb=-pe$*9?O98)rNbtdPU zVea>P>#ZhsX~Y0g67wFZQj$_h9KYXWLstDOav;-+1;iPU^vfU0j2t0F4^dUB?g8RFOKh z?9ygu|BX8e)ulUkcMm`R><;$Ro%QZ}clVyXepM!)0jB9{26`S>fpC|4JUaD!ztyPM zZM#;fZmw^n{tVl=>sD6VHJ;C(W2Prb!UcM%-E?)WNhK}RxdKsBB;|?gI))L(3B(uX zg;8EnF&)AXms`V34kAhe@-QNZ8B8kx7)}z(n$zWgnlz{H-8~ow-@3H14s=7s^h~uJ zBT4-=XjXFRk!Pzf2Cv z%2ai>e3O8_ti=AJ%v5BR@P818T9jj2Mhz*UJt;LLSy1Yfur4k0j!F;6#|OVCmW(+o z1&CA_6h(#YR@`Q{n7etJn4Q(ir7MU3-G3cCxTk2UfbV?J2|#CTNvSV^eTl`3N3Wb! z>$1n@NO|C52-zbc0(YNe+v0*PgR6oP4YT0!;VhPViuCB5=$*NUMjm@36F$Yu=_iVU z#h_(EQNSjmQU$0a>Lm&>G@_mXAwe0z@WW(f$uGCb5j6nIY5@-S6;UmKk>}Q~ATs$+ z9+1f*cwo!OL0Sq0yaycV1H80=z)!bbt8VYy9AYc*>8P-tYnlW3SyiMbe5BwSc%x(R z_o)twyZA&Q_z!*#!w8W@BRQN>IG0b2shP^gi8*~Oa=&INwPKm|RbJJn_GiXYmaCY$ zDlx*cx$0zx3w~E)-39!!=7X8T9G1m_;agW7B~bl$0yn9|>Zj~V`-2a%A4_q)b6qmh z-C`>H#>5ObT=A zj$U0ggSkKrGbF3m`+tq2jLo}8DSDYV@lPE2}#dI zJ{pZA`grZ%|M&jhU@&-a?;a$G?X6YA)T%WHF@tF!G?LR8Fdmz$#6yT;XKUTDjMLue z!M**LU%p{G)?07idgou>$#ku|){MiXRd-MNv%zFym>3y=*UYvn>l^3(^iTiv&fUA+ zRvifB#xq;CX=w^=YaqxC0|HByXS2yeU%a-n@FQK5REmyBM+b0^!eTZat#sFpPUkm2c)z>a?Yic{;py6D=lrGZ z(QJ;*+Z!#8PRBu**`_*gLQGPxY}FTDy0%uo|6p$sZ8)0Tt{Ah~oYJasDAe;a#w?A< ziaqx|jIi2itusMAg5Wn5KQ{^-0CoSJ*Bx@E#ioy81S1h=X%x(Ww=n(|RfAz;!iiIV z(kw}2Q(LX5Ra@g;ENmKNNht+9g2J387xe&qo#RF{jFbH`D~%~dlp_KaZnh(`>BXLvmq|mcbkbRFN79)9 zz`rUh9mEDs=oI#1pS_%fTkAKHnkfki?n%Uzrz4+}Xf5@o{8s21z z8Y{6pGcH%Bcg;!)*pQ^mS2L*pR-~oMSwoB`n8^|s9aWJ-N&efo2;`ldnzC(7oyj*B z^;hg@&7+0^ArW!0>Jc}a*t7sBS-D<(vd6D#06xtlj+4$xx7X{D??x0UREWyqlgz-l zhfxTQy`b)35&1ik2ms!M`X9Nj6@nvI0N+!M8vQ1W=YHmVhf}!62Bdp%)rukm3Tvv$ zQv@=d;)=)u+nh$U;|c?BZ2Vk>QmHI8#+xBL0JmC}JseCsD;8B9$C+zXRFuS}hI7ZbrIQb~smfbrY>4tOR+{R>p5Myc@%*KvaQTivdHK&&QxV9Jv+P`oD zQ4lMNK`Bq<;s-D!yN(Uiy}P?NnJ(fa+}%I9`pgAD?X|TQrHN6wEz{`z@;oS}GjH?U z%Aft!x7+Qt`7FG&zOkV@SrXjYJ;Ks_aig(urQ^rR>0rLQJ9y*0qf6&Dw^q)5_nlj9 zTWdP@d_1eIH-I)bwzh%4;~b$*lCdCT+Pk;zw_A;^^)<*IqMPe@H1^}P-l*d|P0N@} zJ*Mcqu@Ws3Dx?6)HpvifQO{{zh2(_e4Ci4G$FgqYTsKy>)hq&A4R*Xg!9@;C0DRp6 zGE&@%nI`}Ws;gE^&GRG6G$YRkv5=U?2xiCADc}uwT#UbftGq3N{3%ik ziVq?Fef#753!NgAUhyq`->C5~T_85#3NT2aN{ZWPqMeHTh&$(o&e35q@ zTyp<{GnSTAdPXe{xEH^mv`a8soPAdbvWrrRD9V9;bS3zKO03MwFP7OcLJ|Oevw88W zuP{>CWLcAyi~rWYV>#}@AN_ujmlS(JBsv|0vWzpzR$=_SMFdi22R*)dmgDz`Fyr6L ze>ayh$TE_-42;SSX!IQ;sca(fXeO0oD}tY58R_K9E5l!6Oe(`Bw)`%sD6_G|>ElVl z)XdRXmhmK{Ty#U@Eu86(oR?7nk0V9WmpJxfnNB72Fjg$DaKVAUNEQ;jKF9rx<9^Zc zO`-B&$`=5XKx@Ae(n0w;iNbL*k=6O}0ZRfvf6yV#dUdB>Z!r1Rle={_FX>+Fhj$R` zLz#qvkc!yrv9~<-{71e1m^UJy;E{~G2*;g~2lZSVyxFh6WXE=llK@a-RLB&{I&SJLWLd$5>W zd$yR}iOR!(O#a9)eX^1K_}Bf1Ao~jQSu1rSIo0r#xH*{$FQhZ|N!XVCTu1V!{klw! zvoy)b>sXh}rkwW@A(PVmNeSJ_qp0%J%;XQfZb*90V$4!KP)Bc$+G-b!-q)={Y z`v^BM#$+Cj2fdX}^TES| z87&_E+b&H}E;zTU&t9 zuIp%~xdv@QR~*xvj3;*=?l+sYIE*!er7?sHQaYkI#OYUSl}nc|J=lG)+Fjk++?vhj zw{PFY_EAM}QK{OtLanj_A90jrSt~0YghmJ^z?5scIvkFm9^iEhD1}Ls{DUPXlWD`w zec#h`11nB{IMFmS&8YRk)l280Z~WOezva!M-~KzFhs-daE}AtnPBT+gzwyeK5p4v% zukiiU^B1=5K)5+MK3&_|=pCQ5JJtW)|NbA5-A$ecUIEPk?*|~hef!RpOBYGBV7bVS zHBC(`l%u2Ldc_T6K)%3jTUerP$LJl8bEeKFi+ZhcdeS?$wSi?B5fM@E5(Py(o296^ zdR#Qf94E`iix?7*Zd4fMgFyk%X)2JIBj4Y>x3;l0na_82c1Dwh>r`(a?tk(6x$nO< z&K33gg?fKDl*g>yY+t9hcf zAPiG-tz+OmNArajMz-xj^URbCQ2}?&0;p4ffr;e!6iY%;d>o*EV%~%Bb&|e(`NFju z&(%7eQC_@y*c%?~`3HN!;bAfwq>EV^Pzg0TL>9bkQszL4JUTh{7ofjH|BqlgxpN}; z?yf4WE_13(H01N238w$-6?#G8?WcHF39l&SJb%toXq|ECs*{&p zsYDQwF9~ju+>$>A`2}=iXE`_@iU!J)5~oOo4jDD$wH4;`R%VKnR~q&D|Lh;RwMy|{ z|L27&ot?78OKEv8RaTs3hVhZerLrsCvW!_M7P<7HlqXRdB0eXRESu^U%6ui!C5ie` zJ-%_55OyiBWRJx+XP0RCJ`VNx=d$F~gjiLRN{I)Qy%r(V33Juw!oDT4nQo?P9KeMU>%+XAv%b{O8`%A05F|SbM7t~MIdEp`5^9{ zw{$dgmE*VEFpQ!I0sTN-n$$Sf2Q((iTq8oAXCcy>F7v&PcXg*kXW#b`5CHk(ll>sp zHL4 zz3E=LdTD*L|X>zX#`kNXcn?Psle z<@1LGTNup>5t|llgARB69RRowAoKFHdm*x_=5{X0_ z<;m#^>W3x$cu90=hxW1WD}p3znda~Q_HSOi{>=TE_wH;l81%dc_rrs|XfRA?W5n*{ z^8@jU+-~^1r&diRf}!j=n%U{H^;TXYa4O+V)y;G2+9pCkCR%MLF`*Sg_T=S{7a`-Z zBoUyWaxL=mp9Hom?R_Kx?Uvk+yyTT>z2|u-1n=@?TDfTdz@Z*1`^XARzjTLGIHRv{ zxX*1RslO1_;Lvi#BR}5HOXP%b>?q5kR;~PmqBNYM58FDd8X@t17B;v25#^fBoM)`jbD%PEL3x z0$l}iBH|xJQI*KVVUISytl;Cbi~#7E5KS;~pvy_Z8-NOqEjKL5plK1(9enw z7aJ^RmBIrd!8brLOS%hC@@BrkCVx{w~J1dk4ql@%h4E$aceEKu4CVRGiG zX=Rb?rAA5!OFQ$2u z@_QjwBG%SH>T8jLB|xEyE+ZQ(@|htu0)z>ATFsP1b~5*i?%{V%rXw-A9+2H6PeaB-uye!(}QIAz4_Yb+C?=F^FU=LIb7xZ3-hI} z_zBKSe(Dn69zCAsJ^%sZsfuLhALe`;WdJWaWt7WvqWo2EnL;MYK<5CIVta%p0E`Zv zJBgE8#hK0)ijtFMq(b_EX&7_QC)avnJC!JjRn^kDw`LNN2QWpRszK5}C+9L=MM&W$ z17oX2K^P~5L>F^tLsA%|ghUi&Wu-nHR(&r)u-LAdACZYh z846@nN{QR}Nyai6ViFz4dU`Uz??q8;Q{@~v0(H6_xir8|c#mGXt1vTN*XAu0^!a?= z?zEyP;KeS0Knc^iffCkq-a8As_A}3%zjf<=y|ro?y5|R$ZqH}_#zxz%)@Fmut!h~I z(wK6IG;$vzrZS1CtUe0jzz4u4jf{?7H zhiST)1g5NPZL}ZUIb~U>7+NNh!ZBQfsbVb4uuSu|;_cX|qDJk-D`000mGNklwWR7WgQCzFos*V^$ z=1eyerKZ^N5L^UdqL3GC@t?WUwHY& zz_h;K>mN;~)1&#WAnhr4;|{taTUF5T`uVCzFKL56_C@tbfk8m$PJ+;taRZ&t;k8aYDcxte;AH`5X$wPl0TV@^Y?&<1Gst<`|CxOxUmoPLl2&j{JP#m~ zL=;pyd-60Yz6F(;?(ETC#SgvAo)KC1JSt zDawO1#jQ?^NS=z{<402-L(cbWSu|7=q&pJ`KNq@%$d3`cemR<=pDBXyUN?AdYmgTN8*EmWjB{C|+eZhD@M_he@Kv5VK z$)fc#m4k|I(YajONtJN8!%D0LlwQ80o(VrCHTv^; z{@5#YDFtya6BU!OM6PH>P0CJEx(akd0~yH4VZj;*m?$Nspw>FkqB3pXs7+rF^of;4 zOwmUE!k&3V`815DK#P&^SrQ^)J;501cL@F%e|6 z4?W4b{J4$r=lOVg0|4QPt`uUD6RQVm0T;XCIsi1F!HN7PBHswZ2!a5o*c6cZWHY0I z-H)+>Qd1SP#1a+7A(i5RSEh<&E#lqa4M06F*xx_c-r5 zvg`!ANz0mS==wYFynXrdCE!wwM8?y;(~OFMfs7R1!iG>tUMRU<$l19ISKfN#_U`W9 z+IsuzuY7Slo(=}Xzwz7MXwmZgaS(?E4Sx}UnWu|FsV0jcdhlTH%9Shq)6=!JZVF&a zl1!AR0gvev!>GiGht(HBNPn=qy|KBu)?N6Ef&}H1nut{#oNTRLMa+dO2L$9+%q$Nq z#|*|Z(C3p;c=ht-FbrJR%%T*Rgd-0K=<0Ad>Gw|oji+AT>@@m=#rFC2gWkzAm(NL5 z0H7>cU7EM=y3^6HHS}anIf+$o)P-L4H&&9%V@%j)|u2mbVg>#bgB+$~duIs~o4{vHV zYZx)iaB$=iuNsdM$uiAZ5HA)U;tIo1{Sa}NMonfJaonnEA|k|G)e3Cgs%2U9OC);b z+`sh69?R_UPyASh0ytqqW}koN>gS(-)@-zn!sy`S^t5;6@9zi4hski5%qDpd@P@wp zcLaK796EEPn(+p=1y_`~s6<-^Zduu~l}4L2+p=L2x{4#wsEb$nQXe-eV@yQd7x(qg z5;FAUZG(9+-dTa~V@+ir&nGFpI?KNnBCe9;vVxO+B+0vUiWL6w?2PCw-|(T#{?bOC z7yb&}fFpj9KEn0PW#&=NOZ-L5z_I7DE?DA94u{%HuT4<}{8&h^ybyW$S!G6&be2NH zKPl79=;&|`jjYztYZa?nx3Ju(s^|O1hX-Nr0Kq-4d!&ty+olSdlI4_QQWWLpM;l?v z8x%+)Ek?uvV&g8qRrKUyY*{fuB|Zx8M<5y?EGG`t&MxzQmM<+YN>M5p0#Gk6*4dRR z+ep)w3&_v=53r?_kV(t5U0vhjX-DfYN^Q zvoJs^O1oH5PQY^Br$Pi0^s%C~^RkveaECmS9Q^+O@DKl^|EQv=?~i9fefs7Hx97KS z7U2`SbwzIkYNjXTcu5B?iwpM{XnFFVBF`cBID5KV?v5)VfxTGnrf;vG(!xS^h~!`|PKhDyfoD2#{h!E+RG1Q@7~H zi+6qFSv_`qiW51@a+WptNzSGCl>+c79uSigO5-Vb_eI=_w6IXaRBYiKpaSja23|nq zE%|;-&6*U-mIi7>&`uOTlYnY0)1qt^+7Ur%CrOTfjj_WbzyLJJvV<^&qJ&{ceH;^_ z(^TplOpL8a@5Hw{uI10Ccz`OFc-1W7?|F#LaJ^jx=X&e*;gyRUnv#sC;o))r@`at_ z!9cN`>(5-8tgHfAR}Aa9=gtkM!Dul2+)FR~(Rlpu;P}$TozvqZy@-dSQJneM6m(`r zK?3Ngfz;>C&iW^VWLt6mGMI3m1I`)GEqwU&8yVcZ9vkwe8ANP9GTGJfQ zVlSYMUr7`#{0K;9@8*4{YT_V0KORr#nxPpMNTAfoT|ZDNjmEvZw>P_A)YN=urJIM5 zh8dNIh@lp)-f|@vwhzaE!IPKLK-NQBHYAVhF?~&Ir){ zFa~A{`PGp$*-XhSd6iMJ_Fb(^3IEsg3Ft56tS%zX2 z*5>Le#ylpe8rngsGcDH@TE}t-Wh%6)lR=+%f=ejPi>2IQoN_;sG)4@IE9d#4mgtC} zHB~Nzw)`3N0r;`+rwK+ASXKTPf9r2{H@7Bv{{Cdz!-(uZ2oDdUUN0Jtb4WmON-cPK zT1!C_AZc&suKcaIkFXV-Ays@J++l8icSC?24eZe@{&wV_pUl=F*OIuG# zRQ-x3zQX%uiBBJc$|e4mO2D{`VpvJ==e)ZUr~DNG`UTBT;fg}(!&O!7C`wDp*7B^5 za8PA7%iTOS^Tdc#Ez1o?g@oU+8voR8b6C8zm|SL$i(^;{4|$37IixT6&oe$&F0n9& zlx!Jq@sR1Pg0py(@*~cQgK*)wBZ`-ql*wA$KlRdFo=u~Xozxq?Q_Z}&zxyB@_T$;) zd@e}+vy2wOwnJo*%Q}O{t~#%!D|q@%aW*|jLb4&7{pIpn6C4pZ?^n} z;_P2Vc)&#f4yVg^@Dp5~m%Ka}w!E;sT0vUU@8ua3JjIR&UL;W#lCX9uM4A#eS6cR9 zrb7{70DnUBk#}e=3x!J&fK&dYP{~54D*tI22IN%D_*jroNM-QJEeP}cw|@QC&N=pp z=N)=}*EU{#c(DJizsSZX2w9~ke5fJrauPFXG02RRh4?PKQd(pcM0tvo>iiMBDTS0X zsQKaNqNJPWUP)r%kjojBNg<19Ty7UeZa6PkQ+23HI>_yU!eMICt>${Jl1qe;sS7@N zlt+Q}nP&BwKL6+|#HTMaX?W1CCTh!w?nmGlfe7)ogqg*>(5IQwqKp7l)gzonsMGA& zDs)u~zwMcx$!I6UjZTI_nc^!DuQDqIasNX{?}t9;Q##4{q}3uPd8@#Y*jK7S;WGpp zg+jprTNcCov`6gc^c=TA!cwYvO6f*TsYoqXo6h108(Cl^YEaO{u*mUH%Sep#RK?(l zia9IY_m_E@pKSc;_Rj^uLSrlED$`Rnl9M~sbf@}4cVdlH3S`v~_YT9=b?~jR66s9+ z5$E!x+vCsj@$?1&*ge4-TZX|+crXvhEG3B&ETZJYlk;>C5v^k)kuC+E(cAB_<*%r-VVZ@+!p zZ#Ax6I`_f--M4Suw=0zkSFao%9pAbCpi{4_lC^g*e*U@8w6%h$aRZYo2|@s6VEi~t zKL6sgd%K5Wo|$A;MO})ivX6YG^@D>Gj8dgmi=(jBu2d>6q8&{6$z(Q}%*Ug-Wf_e| zl{%+s%4D(#gUIt2jYci>;=yFlZZ*QdH!UMhVvLT&alh!XixrwkNlhgZE|VR+CyIN$ zUbDKXY3WISx(FRFn3A(h7=!j#4IDXwEF5}rbadM3cK7!lB4DVx4!#lb6jdy!3_3$Z z3^XJhgNI%i=eh4MEZg*>z@YYCpuBYeVj!LKJKKZd#B!{A_aDaDdM;5KLKMy?(}ii* zZoPlI-CkYHhN|Xtny#kH-A=nd9Chp_rp5m8bT%7StFr6b*RNi%=^XXMPB%RNf1o=dNSE;NXYHd$w&}Sg)T9MsY4pMxJ9S2K7mk5VvWRpfAt-XyV08R>Q@tRd-F2 zxC+&zu$Jd86Zpue9WRxwa{*e3D9@Yr$AuR^du1*0LTFqTeMWob000mGNkl$D-KsllIHu!9Wl1YXG!nX!tO1=jM zMIgLf{)zp?KjCA!xAF7`nSJ&8)!+EW*CME_esHw#PESw#{oQbH4{}&CndMPP5Jw2T z0Ai{tS}Em14$OG(y*w?JIEiPV$Vz)n-Ppou0OHe~oIh61yL9uEM56y$jW0p}g6DdO zyzNJ9ap~#FmL&2c+w?NYUzol#?%bj=_U~7;E!TE5wIWN73~2=rACLzsHQ3QY!=_Dr zCpl3rrDe4!=h>+r5va$6db55d9G{EB|66k>=9ww-GJ#!C^^Y;zl>RD9A{m(R@LO7z zJPBu>LXhfGiIj`wiJaxn3#`u`Dd3NGEFr#QEikhAEKajP)m6tyh9k4p!bnZ;yoce= z>#agi;7nj_!QP83d;!FZvk#V*DN>Z?Sd{q-%UJ4cU`y^^Z1JE zlJjRv(oncP@#v`x;)*gzD)KTOVP{1Myz?%1lICv|hf$Pq3s1X}M6sVDevN-ZAuy`U zb!nNGI>;g^xTNrNo|>J@LMx!GAxucU@FB`u><&g4iN-I-x|ah|EhddyP!Y7+NA zrK}gh|7;F$?ww!>c=MUwU@)uU@

KpePRV7MO+^XA#y7TwE8f@M~YZ@r@Usz111t-ap-GTQ59w{++w~Y&Ybe8 ztpKLGE+Pdp2z;`maUD>@_@X4v0lqOyb;F1-G-MH)%A##2)w~f*#P9>$x4TWpc5wQQ zx_WXtw=FA9{5TqCX_EowNTB1vYZ{~J0BrJXIx{T`f&j*lbi9IlPN=e`7`&3RNTLkT zRAa?(0P3A=^1XSiS_L>EGK0wp1Xr8Ra57{vgdRH#7q;nu5w2D=r?R>5bMBk(tgO0o zFI?+ZCE3UVR;#Z)+`KP;(&u$ zmXcX#ng@}g>pTsN1-@t6D)k|zJR)E)$|U!ofJE$YI>VE0v$^o&d%MF=B9$P+tG}%EU zXOeBSTVMIy3zx549wb>Wm>ofaIXMpY9|Xres`s5waf{|@F4$NJ&>vty;ThVzeIw=k za1Q`po=c#LVF{>Il~z}_Y+mCV&>4!>UBbdYjf(`oMR`7jApY@(fLBpkyGYBt66p;0 zNXy)iC46PVd|zbva9*DxSXDT;giNX|X5Uz?VcJtQ0-3Ak+{zOV$`8&6>B}X0Jlhk9 z^W?@aD%UHh*I4@Z=Ld~E`F}LF?y06E8dR0z{NYg-K?z5&d2t4gOC~!%Gvj}ZmXeof zyf||LD%o#V6leKE0tO2RUW8a6a5h*10-sr~Ua1Yf`xmVX7q7nZYhSo<{*V91--{^i z??abgu-M9DMo4^E#a1k23-M|x9*Dy$LxMuQle6kl@lp1SJqjwypM_wK3x2*#Od8P> z;Zf?AxU5Wi^j-WQ%CsVO)&s5lHnucST3+{ZR)~11C_`D12|&R?F0KQY`iU^-VF#1S zynbGGA|!%BF4coawZ+t@lJ}|;F+hQI{gutFfBoP2pDRVSJM$3mV~czH-S@nE z?-y~bR-DH57o{hBa7HVO?NDBkY8R?nokS|TZToL}^2A8F#vvDvykdVeZvN5FX|5|o z!;vBuYW^+WMso{*KcQMQteB8_HE!<6ANg|n6pzi=pTW=4#g3%-L1ynxv7w814sw_O;a&16upkiPj6SO{e%;8iIkunCAjRKH_wq_-9 zs2N%UpcbV-Y0X+?=ls^Kd-uF~sI{uKsyUkHx>4D>P?-*92ZyJONDg z-nwO8!6PiIoiRHpNw)h6Ts(qGdRv9E)^1v~&Imnk#8 zkPMBbq>X^kKRFrTgmhhdaPRKX(ebm-UJE!m8YdBGe%&-X-8NWW%eE0a?Cl+_udiV? z-oAaS-Dx$O%`kw7VTfj^0SO>fpGvi`yxL9X+Dc4S(HcoEOmSR$I++`~O0KG8g(}tS zP2I$?7~{#jTJKW!C*W4193S=8*H=cPv7xz>(b#cZnVgq`;dtEbbaYK5XJx_>hZbAM zv|PuqsOcLpJ^q-wPTA;b0Xd-4Y`*pOd-ZDV)~yFG-gr)7q68EJ6P&-){{27rqu=_C zZ%pU@&G+tUrr94)-CAwu{Dm|E(WJz|qG^M4%D(jZi$}figWah{nfIrdR1Xh=ql2+x z<`rH4#ur|^|KL8ts(QWcpB{Ps97i1F34lCC`r+}>xvlktoTlu8kaR}q4+=WuM&0D8 z0WtSwZF#;w9#7WS)*M5>|KLHr-E}L~cDFn1pDd=+I)ct>Er~~~t8Uj56oQlT-;1t04?{ zas?n?ium$}@86F;vRv2HS6+Vph3Bs4w!P`QzVd;#6QPd zOu(;44i0B%Q92`|mq3+?3=%>4m(hrm}{@%rnR;N>tE3qQ> zuO)h9(pk-`wA`BnHkC^>S&9;F^&2?^uhIWGZ@jBm`o|4wNm+{80O10}Uq8DL0u#$i zZdII0`eA=kdHI<^=%vR!4qJ-Dtn~P=rc}dGDAKDSAv5|C!c326%KMZb+r%HcT;(s*vt>>Z6V?iODMqlz3Szi1 z<3oUbrMZ;lo#ar7Jm?ExCowH4oGzl4EnzbRuW+y{Qd;yATdXlEj@{W$96}bD1dp ziJuoT<>!gGM`4=jGylE7mP}>G3Z%`dQvJ%Uh95L7|iC zk+!O5ec~BGSBcOS`-XnbiH$&9R_t_=LA-*(Opu29Ch&pjy*sPsZp?*6ti+VkCFaf( zj-FKNa*tkD3n=*7`1_vwk|J*?++K%T)+O;6*d? z9t5!|mmPzD$hn;LJ%0)xPjLW1oUsze1XC1hph#M4&b`}tO&l3|@+v-RY-s8l*i)i1 zftm0-NemE4X(kf*a?1F}t_{pcZYK=bAXmsfh_L>`kAUdOYF(mhjF7@I43)8vBL;ke zL_D*C&x+zm(Tz08JwGr_t=n#dNeEuHQ*{GByWg8$xN=#3cyiP`Ik#1l% za%pG%csLIu@6ty1nODB{Cx7wXE9bgje(vIT-@AYN{@zQ^UTjtD-Tf25-jn{g({5H> z3tt+=iD5Vt-YBfqZjm{BUL5QnU%j#em<}~yJUYcO<7B}GzwqJ<&pdNAjzU_Aq%yOB zTE_2V+IY`KFp4N13N3)hLK?+LTaB9lKI*?sYod z^|jUqH@*IF++Ar(%$_Z#^F;)v*fkx`3p(8zIo(V7)>>DUWymjo{GIQ_nX$Kb{O|tT ze^XPLsWuvw>1x$MMEKzF=+{4g>7Re|)hp*#A-mkTu;NdLHOH8{sKb`|W$0WEU zibX(0(JWxTUhfDOrq!@A;zJ9=Fx(25IBK?)`v9!@Vm@VBq3hOg?1gdSIJIEDpsT=G zt65ixY$tJrjD`(URy4~N-#!DJ73I6+08S{tzZ_XwwZ)+000mGNklLS--X;Fho;cysDZih-g$`%j|)6lcY$JA)wX zFZ_Kk7>$SF?)~uSAR3+~qiGiU1^23=>V8#F>|9+X+MZlH3-L!JN#nMy*452(GUhWN zejKxulKW-;lR`C-duh2*V^iysZ z9V96LjJkr;#d zFSD4L^tvqeYDu(tkc+eaW{*D@dlV9|$9|N~Sp~};70~bqfq#l3&VtJ%0+evmBd!%F zQt1wqmmZ^dPZmY-Gtb}n@>jo#jo|iV+Oo{Mlf~|vZzaRyJPe#?uRrs-uXwcIJ=vr3 zdBfP!1Lo!2%sj)&OW*0S^A$s?%)U2wGh1NF|37>00Vc^+oeft`o#XVRotfR8%~=@* zBoq*c985CVBx5j!4=~1nF<_j)*hJ$%6d8#mBmv5Kwc6F@JULJ2?#k&u=T>!3?@GJU z=mUHI=lM#b-kzH7s#~{i-TR*Rob#UWF$gMN(Cwb4LZhgInLg8g%-I9SFM8o=%P=F7 zVmGPU-eydUONEH*7?>O5ixhbJNS3=7rb_z$QV;voALRWhd&mf*Nrf$P|5+R>4(&=^ za^-kKFbSVR%LYt9Wrk|dek}0e!1OG!Fx9ZMVObGIawJDtE7P`DJ5a;gXDkqQA_gZj zPq)XOYCi*uF;i#Cd$o+9@F^^S>+tp;te5`x-xGgpFgb*xn^;kfvbe0I3OesZQ1Zja zqB(4XaW%*4)?+)88nBr)FPKG`r39(AXLOnUk0pj2ffi|^Vapp*b-$kC6Y_A}pc`P> z0&1ao|CH}Oag04-H9U8=bGno?@cuYAI*t&L0=FuVmq0&Ta7%cUipFSo-{S?|(H$c0 z7uI6HL_SOvpi!Wm#auj&MS&om;~eD3QAQI`wSZy$Oj9)!SrR40MuF!V#db(SUmzYT ziO9j3E}#gv%+sl8 zalSM?TZ(1!s;M13Ho0X@|K<(rPMkbmTrBkutU7S$glBMzrAE2hXx2?hgoV#gbKloB zbML-A-~s6F$rGlZA^iq`omW*0SV}>Vs?}ON9;Yb6Co=f(@R)nbl10kKrPuW&Swc69 zL#eu!OeZJDXVfO!sRy(VhXe0K6b=yuL?}+q7t2k3 zdU9IRJ?QmpCbe$eD!AU_e67%xEmew!x|ZruA}Md1#MxbaZj;U=Q>UukgKH zkt0*pf$rE$slIvb;EJBK2-wlK_8lFANnEbG`KaRNawjL}U}}XW5-h#$o|Ox8)l#Ja z*QBXVKF1>0PfIrZwcePSE9JV9;1EF-5P%&lN__f`r8XM9eSI(nP0MOF8fv4NOvS;Z z00@;%XTUWCT|C+t@9pgdFmkeV5y%@77sR2a4%hWm$6;B>oaYgn5Jb0b(%c>vz?xaeTX2x2qAe)Ir< z!`}(=fhTGDTL(v8ZrVeuhiD?mOSNHR$ga|KQ0f(BJz20CeK7Ed`OGER4*d3BlgZj`V|Wq62Z3U`GHmJyMvg!`Fl1 zTI5%c5c=+ui{bjG^nZuTD=>uEps;MqWf8h!2=)jKgRsp(xwe2CB)QQnaa*t?ncbla zG3lMJd?|Q=4_50Dj;-kCz5@r{>674u5qk<-FTWpEJY*}bNZpF3;PSW)JKU^=J1MmG%iXhzY2XmgsMj0>W(TlznsQh#k*h z2#`M0wfsK=;F)ZuZIgOU7@+|7*+%9M7c7S4^6~;RPc zc2lWjQ>{rdaLbmayRw8DK`g4GW8oI?C4=SQ>zM2h5CaPbh!__H2$e+n(zfFQe1{D% zD4=05BESI9O&iu408vpz$)r;CnmJusT(fRPx1B8(8$9n14Rr6@KW&fCU9f%aW4jMT zQt9q&G@Z&EJbJQWX}qTvHD&kF$t%xaziy!C(Ss-CM6S1|P%Blyl(a2xWc2vfZJP|; z-n(yCS0>{)hHfB_z;i;O&}&-WFK)T@x@)cij@~vKnQY#&bwHM2DMX@*Ao9SqScZj0 z5z?XnRJc?!H99`EZuJ06TQJkgGMZB0BGGk&W5rlBF*iGx>w?D5ClU$hK=fJN7t?2Fg`vPi^ulvIehVD=TA({U3~eb%}sb@ zDIROO(t(yJ>VR|L>-0w_%EfZsrCo*>HH}T=vV+OEBq+1fa}gzyN=d!Fk%t~T(UnO? zMIn|;A{^i)U}!GivG(zUCx7(Id$(*@GuT@&Xf2(T0BToC+NM=qHaHw8>*0~bbS9+; z=yL#+*&X-Gic&V+H3R)+wLCY!NV&qu_}rSlv@FRmL^MMmIXPC>n=AVVhu5qw*NUbA z-WOxViZp|9_H@0WaVBrry63>n@WFFp89Eb>0(@$!O$F{#3;{hro6+$}80TOsa%`9s z2^MFwp`sH8#<9o&wvQ)dk@IZb5P6^-fTnkqh*URp&-TETFw~U;>(qC2@KqRC(hInJ z1NkhpwJaDmU<>67U=(RVn6hJG2n~#XSZPt@7z|=C7Kq+Ga9J)qV>ZR12IBHIx?>{k z+)WW>)1)L_xntX=P3tw9nQm%`{_FMT!i+vMW6sW*m9ncgJi|crkNi3bsV9|vk#lxY zq@QsAUQmt~Py~}mGl>)i2x+PtB=sWbw}VVyjQnweWZyt*B~)P#C*-A&9D1qAVR$$l zBK`pVljtLgnbGA(9KN zy70>UrOXG4ROJm$>9>2Lo%04wK3t~@N@~%OX zFp1x%W1{%}H@q$rjR5PiJ;IOZ4-%N#_z1Oy3F?^o;2uY^x(5T zAJLPQeU11Ih0ppH>!8M;%aD{iOm3xIHmoGqilD&T;t^#gQVp9T96(F5cIh+AdVlI3 zQun?!K@g0=fcL`YZLz?c^UxrMXp6uw;Z6mh7TAd^zrg2jfkaxfg6=1&M+KBk^ry(f`LxAEd%B=$73YH2dG}f zVvzzc>VOsi)bhBo0Byl`kU0u#T(JTN=73?uBBF)mH5}lG1d=gqq7#6}02J4kSr!OX zT-R-+{w!m8n(OHy4EQxO^=KX_t*S;g%WQWmm1}_D9EQ-xY1|vP~=HNUT(xOGUXoN2g0ou+yAEAwM7um1@=5 zkrPTJ0st|aODlq+H=01MV+@iKnVl=9(^*;JYxPQ_){vFBrl}kWug4K(TMRAcyK*oV zO~VINSX`{YOp#1R;fCZhSwY|%x~6N*TD=^Rz_MfGDQR@N42#pyK%ys_JU%j=ibMhN zbFN1FPSfIDkLE-G-14Gst6Bq~epFJxYaEm0f!_Ss^qkLdn&aCZCG)I=D;9>t2bEL7 zAz-)`Aut(YC@ynr(W|G)9l(sq$?_|%y(-^bsC(|BZp;~GsZnptO&fsz7Z&Vt$yGJa zHqmj1l@f^L6cY#*IY|6tJdfQC2X|1%E#<|a5B5kzv8lW{XG*+t!Nl4W06DU>i&Iuo$8|7 zY-F}=UB7w_*un>YcWZNUl3M0QMp;yR1~ zY%5gD4-e7+bU=&0`FfW9bjw5{CknG4!1ky~wa3lW`7)hDT@S%0)v)3D@w|?r8%rY# zMj-0ykSZ!P6SO@D(eh9G%_5@Lz-8HyVK!V&8;z*Cyh zt%@J@Xo03}%#uQ@j;4PveR2-Hc6#%0hJc%4%UyEmNJH=>8YQ%l2{3A&Y>E``TfQUo zb5Sl)-P2%oWJDa4#b8<%KmOweg5po3|FN_k*0;tJBf|r>9FA01)P?#{H7dopf@ruZ z#bb&lRwi(WkAi3Xj*<2MasZymhKn+TzI}KD=oX+qU|G|$Q0|jL^a}F@;CeuowhNp+ z1DFZeaNV?ko98SN9!7ZBs2x5u#;j*HZXTXrsE&@$ZCcx3 zQ_VZ?JFs=z>J4j$0T+TDvVOzh{LI3^edE%0DXJI;kIu)V($>wZ9yjYpj!noMuZWE4 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 bool: +async def _is_quota_exhausted(returncode: int, stdout: str, stderr: str) -> bool: """ Detect quota exhaustion from agy subprocess results. @@ -152,8 +153,9 @@ def _is_quota_exhausted(returncode: int, stdout: str, stderr: str) -> bool: log_path = os.path.expanduser("~/.gemini/antigravity-cli/cli.log") try: if os.path.exists(log_path): - with open(log_path, "r") as f: - for line in f.readlines()[-5:]: + async with aiofiles.open(log_path, "r") as f: + lines = await f.readlines() + for line in lines[-5:]: if "RESOURCE_EXHAUSTED" in line or "code 429" in line: return True except Exception: @@ -346,8 +348,9 @@ async def try_agy_proxy(prompt: str, messages: list = None, rc = 0 if raw_rc is None else raw_rc raw_stderr = first_data.get("stderr", "") stderr_content = "" if raw_stderr is None else raw_stderr - if _is_quota_exhausted(rc, "", stderr_content) or rc != 0: - if _is_quota_exhausted(rc, "", stderr_content): + is_exhausted = await _is_quota_exhausted(rc, "", stderr_content) + if is_exhausted or rc != 0: + if is_exhausted: tier_breaker.record_failure() if cooldown_persistence is not None: try: @@ -420,7 +423,7 @@ async def token_generator(stream_resp, httpx_client, initial_line, current_conv_ last_conv_id = result_conv_id # Check for quota exhaustion - if _is_quota_exhausted(returncode, stdout, stderr): + if await _is_quota_exhausted(returncode, stdout, stderr): tier_breaker.record_failure() if cooldown_persistence is not None: try: diff --git a/router/tests/test_agy_proxy.py b/router/tests/test_agy_proxy.py index 404059eb..f4cd540d 100644 --- a/router/tests/test_agy_proxy.py +++ b/router/tests/test_agy_proxy.py @@ -1,4 +1,5 @@ -from unittest.mock import patch, MagicMock +import pytest +from unittest.mock import patch, MagicMock, AsyncMock from router.agy_proxy import _wrap_response, _is_quota_exhausted def test_wrap_response_basic(): @@ -58,45 +59,51 @@ def test_wrap_response_long_strings(): assert result["usage"]["completion_tokens"] == 250 assert result["usage"]["total_tokens"] == 375 -def test_is_quota_exhausted_stderr_markers(): +@pytest.mark.asyncio +async def test_is_quota_exhausted_stderr_markers(): markers = ["RESOURCE_EXHAUSTED", "code 429", "quota reached", "rate limit"] for marker in markers: - assert _is_quota_exhausted(0, "", marker) is True - assert _is_quota_exhausted(1, "", f"Error: {marker}") is True + assert await _is_quota_exhausted(0, "", marker) is True + assert await _is_quota_exhausted(1, "", f"Error: {marker}") is True -def test_is_quota_exhausted_success(): - assert _is_quota_exhausted(0, "some valid response", "") is False +@pytest.mark.asyncio +async def test_is_quota_exhausted_success(): + assert await _is_quota_exhausted(0, "some valid response", "") is False -def test_is_quota_exhausted_other_error(): - assert _is_quota_exhausted(1, "", "some other random error") is False +@pytest.mark.asyncio +async def test_is_quota_exhausted_other_error(): + assert await _is_quota_exhausted(1, "", "some other random error") is False @patch("router.agy_proxy.os.path.exists") -@patch("builtins.open") +@patch("aiofiles.open") @patch("router.agy_proxy.time.time") -def test_is_quota_exhausted_empty_reads_log(mock_time, mock_open, mock_exists): +@pytest.mark.asyncio +async def test_is_quota_exhausted_empty_reads_log(mock_time, mock_open, mock_exists): mock_time.return_value = 1000.0 mock_exists.return_value = True - mock_file = MagicMock() + mock_file = AsyncMock() mock_file.readlines.return_value = ["line 1\n", "RESOURCE_EXHAUSTED info\n", "line 3\n"] - mock_open.return_value.__enter__.return_value = mock_file + mock_open.return_value.__aenter__.return_value = mock_file with patch("router.agy_proxy._last_log_check", 0): - assert _is_quota_exhausted(0, "", "") is True + assert await _is_quota_exhausted(0, "", "") is True @patch("router.agy_proxy.time.time") -def test_is_quota_exhausted_empty_throttled(mock_time): +@pytest.mark.asyncio +async def test_is_quota_exhausted_empty_throttled(mock_time): # Set time diff to be < 2.0 mock_time.return_value = 1001.0 with patch("router.agy_proxy._last_log_check", 1000.0): # Even without reading log, falls back to True - assert _is_quota_exhausted(0, "", "") is True + assert await _is_quota_exhausted(0, "", "") is True @patch("router.agy_proxy.os.path.exists") @patch("router.agy_proxy.time.time") -def test_is_quota_exhausted_empty_no_log_fallback(mock_time, mock_exists): +@pytest.mark.asyncio +async def test_is_quota_exhausted_empty_no_log_fallback(mock_time, mock_exists): mock_time.return_value = 1000.0 mock_exists.return_value = False with patch("router.agy_proxy._last_log_check", 0): - assert _is_quota_exhausted(0, "", "") is True + assert await _is_quota_exhausted(0, "", "") is True diff --git a/router/tests/test_detect_active_tool.py b/router/tests/test_detect_active_tool.py index 3105ab1e..c5b359d1 100644 --- a/router/tests/test_detect_active_tool.py +++ b/router/tests/test_detect_active_tool.py @@ -9,7 +9,7 @@ # Add the parent directory to the path so we can import from router sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from router.main import detect_active_tool +from main import detect_active_tool def test_detect_active_tool_empty(): assert detect_active_tool({}) == "none" diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py index cb1f5a4a..9ff795b4 100644 --- a/scripts/benchmark_classifier.py +++ b/scripts/benchmark_classifier.py @@ -54,9 +54,11 @@ def classify(prompt): per_tier = {t: {"correct": 0, "total": 0} for t in TIERS} confusion = defaultdict(Counter) # confusion[expected][predicted] -for i, item in enumerate(dataset.get("prompts", [])): +import concurrent.futures +import threading + +def process_item(item): prompt = item["prompt"] - # Support both old schema ("tier") and new schema ("llm_tier" / "clf_tier") expected = item.get("tier") or item.get("llm_tier") or item.get("clf_tier", "") try: @@ -64,31 +66,50 @@ def classify(prompt): except Exception as e: predicted = f"ERROR: {str(e)[:50]}" - results.append({ - "prompt": prompt[:100], - "expected": expected, - "predicted": predicted, - }) + return expected, predicted + +results_list = [None] * total - # Only score against known tiers — skip ERROR/unknown labels gracefully - if expected not in per_tier: - confusion[expected][predicted] += 1 - continue +with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor: + rate_limit_lock = threading.Lock() + + def process_item_with_rate_limit(index_and_item): + i, item = index_and_item + with rate_limit_lock: + time.sleep(0.05) - per_tier[expected]["total"] += 1 - if predicted == expected: - correct += 1 - per_tier[expected]["correct"] += 1 + expected, predicted = process_item(item) + return i, item, expected, predicted - confusion[expected][predicted] += 1 + futures = [executor.submit(process_item_with_rate_limit, (i, item)) for i, item in enumerate(dataset.get("prompts", []))] - # Progress - if (i + 1) % 20 == 0: - scored_so_far = sum(t["total"] for t in per_tier.values()) - acc = (correct / scored_so_far * 100) if scored_so_far > 0 else 0.0 - print(f" {i+1}/{total} — accuracy {acc:.1f}%") + completed_count = 0 + for future in concurrent.futures.as_completed(futures): + i, item, expected, predicted = future.result() + + results_list[i] = { + "prompt": item["prompt"][:100], + "expected": expected, + "predicted": predicted, + } + + if expected not in per_tier: + confusion[expected][predicted] += 1 + else: + per_tier[expected]["total"] += 1 + if predicted == expected: + correct += 1 + per_tier[expected]["correct"] += 1 + confusion[expected][predicted] += 1 + + completed_count += 1 + + if completed_count % 20 == 0: + scored_so_far = sum(t["total"] for t in per_tier.values()) + acc = (correct / scored_so_far * 100) if scored_so_far > 0 else 0.0 + print(f" {completed_count}/{total} — accuracy {acc:.1f}%") - time.sleep(0.05) # minimal rate-limit (model handles concurrency via llama-server slots) +results = [r for r in results_list if r is not None] # Report scored_total = sum(t["total"] for t in per_tier.values()) diff --git a/scripts/get_pr_status.py b/scripts/get_pr_status.py index d65f65f1..c64bac43 100644 --- a/scripts/get_pr_status.py +++ b/scripts/get_pr_status.py @@ -24,7 +24,7 @@ def get_pr_status(pr_id: str = "") -> None: state = data.get("state") review = data.get("reviewDecision") or "NONE" - checks = data.get("statusCheckRollup", []) + checks = data.get("statusCheckRollup") or [] # Summarize checks success_count = 0 diff --git a/start-stack.sh b/start-stack.sh index 03bb1220..bfd17edf 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -95,7 +95,7 @@ else echo "⚠️ Warning: Host agy daemon not responding on port 5005" fi -if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ] || [ -z "$MINIO_ROOT_USER" ] || [ -z "$MINIO_ROOT_PASSWORD" ]; then +if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ] || [ -z "$MINIO_ROOT_USER" ] || [ -z "$MINIO_ROOT_PASSWORD" ] || [ -z "$LANGFUSE_INIT_USER_PASSWORD" ] || [ -z "$REDIS_AUTH" ] || [ -z "$CLICKHOUSE_PASSWORD" ]; then if ! command -v openssl &>/dev/null; then echo "❌ Error: 'openssl' is required to generate secure random keys but was not found in PATH." exit 1 @@ -398,7 +398,7 @@ placeholders = [ "ENCRYPTION_KEY_PLACEHOLDER", "postgres-password-***", "MINIO_USER_PLACEHOLDER", - "MINIO_PASSWORD_PLACEHOLDER" + "MINIO_PASSWORD_PLACEHOLDER", "LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER" ] for ph in placeholders: diff --git a/tests/test_get_pr_status.py b/tests/test_get_pr_status.py index 93885a14..f5fcbf55 100644 --- a/tests/test_get_pr_status.py +++ b/tests/test_get_pr_status.py @@ -82,3 +82,20 @@ def test_get_pr_status_invalid_json(mock_run_cmd, capsys): assert e.value.code == 1 captured = capsys.readouterr() assert "Error: Failed to parse gh CLI output" in captured.err + + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_null_checks(mock_run_cmd, capsys): + mock_data = { + "state": "OPEN", + "reviewDecision": "REVIEW_REQUIRED", + "statusCheckRollup": None + } + mock_run_cmd.return_value = json.dumps(mock_data) + + get_pr_status("123") + + captured = capsys.readouterr() + assert "PR Status: OPEN" in captured.out + assert "Review Decision: REVIEW_REQUIRED" in captured.out + assert "Checks: 0/0 passed" in captured.out From 6cdf604443cccac6376f12eba80070ef7cb3d763 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 23:54:42 +0200 Subject: [PATCH 08/22] chore: restore clean secret placeholders in pod.yaml/start-stack.sh and Prisma serialization monkey-patch --- litellm/entrypoint.py | 20 +++++++++++ pod.yaml | 34 +++++++++---------- start-stack.sh | 77 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/litellm/entrypoint.py b/litellm/entrypoint.py index 1b987365..20d4baf0 100644 --- a/litellm/entrypoint.py +++ b/litellm/entrypoint.py @@ -98,6 +98,26 @@ def strptime(cls, date_str: str, fmt: str) -> original_datetime: datetime.datetime = RobustDatetime sys.stdout.flush() +# Register both RobustDatetime AND the original datetime with Prisma's +# singledispatch serializer. When entrypoint.py replaces datetime.datetime +# with RobustDatetime before Prisma loads, Prisma's own +# @serializer.register(datetime.datetime) ends up registering RobustDatetime. +# But database drivers (psycopg2) return the *original* C-level datetime +# instances, which no longer match. We must register both classes. +try: + from prisma.builder import serializer + def _serialize_dt(dt): + """Serialize datetime to ISO8601 with timezone (UTC if naive).""" + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat() + serializer.register(original_datetime, _serialize_dt) + serializer.register(RobustDatetime, _serialize_dt) + print("🩹 Registered original_datetime + RobustDatetime with Prisma serializer") +except Exception: + pass +sys.stdout.flush() + # Start LiteLLM Proxy import litellm from litellm.proxy.proxy_cli import run_server diff --git a/pod.yaml b/pod.yaml index 28a5b572..b8ed35b8 100644 --- a/pod.yaml +++ b/pod.yaml @@ -4,7 +4,7 @@ metadata: name: agent-router-pod spec: containers: - # Valkey Cache 9.1.0-alpine — LiteLLM response cache (exact-match & semantic) + # Valkey Cache — LiteLLM response cache (exact-match & semantic) - command: - valkey-server - --protected-mode @@ -34,7 +34,7 @@ spec: - /app/entrypoint.py env: - name: DATABASE_URL - value: postgresql://postgres:***@127.0.0.1:5432/postgres + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/postgres - name: STORE_MODEL_IN_DB value: 'True' - name: LITELLM_LOG @@ -42,9 +42,9 @@ spec: - name: LANGFUSE_HOST value: http://127.0.0.1:3001 - name: LITELLM_MASTER_KEY - value: sk-lit...33bf + value: LITELLM_MASTER_KEY_PLACEHOLDER - name: OLLAMA_API_KEY - value: 3cd542f8fca14fd08bedfd4f2ab36f9f.6G7Iukbvu1Keyi9x8eKckNEO + value: OLLAMA_API_KEY_PLACEHOLDER image: ghcr.io/berriai/litellm:v1.90.2 livenessProbe: exec: @@ -89,19 +89,19 @@ spec: - name: LITELLM_CONFIG_PATH value: /config/litellm_dir/config.yaml - name: DATABASE_URL - value: postgresql://postgres:***@127.0.0.1:5432/postgres + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/postgres - name: DBUS_SESSION_BUS_ADDRESS value: unix:path=/run/user/1000/bus - name: LITELLM_MASTER_KEY - value: sk-lit...33bf + value: LITELLM_MASTER_KEY_PLACEHOLDER - name: LANGFUSE_PUBLIC_KEY - value: pk-lf-200bbf48-1d23-447d-a79a-8614106497e6 + value: LANGFUSE_PUBLIC_KEY_PLACEHOLDER - name: LANGFUSE_SECRET_KEY - value: sk-lf-...3b49 + value: LANGFUSE_SECRET_KEY_PLACEHOLDER - name: LANGFUSE_HOST value: http://127.0.0.1:3001 - name: OLLAMA_API_KEY - value: 3cd542f8fca14fd08bedfd4f2ab36f9f.6G7Iukbvu1Keyi9x8eKckNEO + value: OLLAMA_API_KEY_PLACEHOLDER - name: LOG_LEVEL value: info image: localhost/llm-triage-router:latest @@ -154,10 +154,10 @@ spec: - name: POSTGRES_USER value: postgres - name: POSTGRES_PASSWORD - value: postgres-password-*** + value: POSTGRES_PASSWORD_RAW_PLACEHOLDER - name: POSTGRES_DB value: langfuse - image: docker.io/pgvector/pgvector:pg18 + image: docker.io/pgvector/pgvector:0.8.3-pg18 livenessProbe: exec: command: @@ -188,7 +188,7 @@ spec: value: clickhouse - name: CLICKHOUSE_PASSWORD value: clickhouse - image: docker.io/clickhouse/clickhouse-server:26.5.1 + image: docker.io/clickhouse/clickhouse-server:26.6.1.1193 livenessProbe: exec: command: @@ -253,7 +253,7 @@ spec: # Langfuse Web — observability dashboard - env: - name: DATABASE_URL - value: postgresql://postgres:***@127.0.0.1:5432/langfuse + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/langfuse - name: NEXTAUTH_SECRET value: NEXTAUTH_SECRET_PLACEHOLDER - name: NEXTAUTH_URL @@ -310,7 +310,7 @@ spec: value: LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER - name: LANGFUSE_LOG_LEVEL value: warn - image: docker.io/langfuse/langfuse:3 + image: docker.io/langfuse/langfuse:3.202.1 livenessProbe: exec: command: @@ -337,7 +337,7 @@ spec: # Langfuse Worker — background job processor - env: - name: DATABASE_URL - value: postgresql://postgres:***@127.0.0.1:5432/langfuse + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/langfuse - name: CLICKHOUSE_URL value: http://127.0.0.1:8123 - name: CLICKHOUSE_USER @@ -372,7 +372,7 @@ spec: value: MINIO_PASSWORD_PLACEHOLDER - name: LANGFUSE_LOG_LEVEL value: warn - image: docker.io/langfuse/langfuse-worker:3 + image: docker.io/langfuse/langfuse-worker:3.202.1 livenessProbe: exec: command: @@ -396,7 +396,7 @@ spec: value: MINIO_USER_PLACEHOLDER - name: MINIO_ROOT_PASSWORD value: MINIO_PASSWORD_PLACEHOLDER - image: docker.io/minio/minio:latest + image: docker.io/minio/minio:RELEASE.2025-09-07T16-13-09Z livenessProbe: httpGet: path: /minio/health/live diff --git a/start-stack.sh b/start-stack.sh index bfd17edf..1f751823 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -95,7 +95,7 @@ else echo "⚠️ Warning: Host agy daemon not responding on port 5005" fi -if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ] || [ -z "$MINIO_ROOT_USER" ] || [ -z "$MINIO_ROOT_PASSWORD" ] || [ -z "$LANGFUSE_INIT_USER_PASSWORD" ] || [ -z "$REDIS_AUTH" ] || [ -z "$CLICKHOUSE_PASSWORD" ]; then +if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ] || [ -z "$MINIO_ROOT_USER" ] || [ -z "$MINIO_ROOT_PASSWORD" ] || [ -z "$LANGFUSE_INIT_USER_PASSWORD" ] || [ -z "$REDIS_AUTH" ] || [ -z "$CLICKHOUSE_PASSWORD" ] || [ -z "$LANGFUSE_PUBLIC_KEY" ] || [ -z "$LANGFUSE_SECRET_KEY" ]; then if ! command -v openssl &>/dev/null; then echo "❌ Error: 'openssl' is required to generate secure random keys but was not found in PATH." exit 1 @@ -106,6 +106,17 @@ fi touch "$ENV_FILE" chmod 600 "$ENV_FILE" +generate_uuid() { + local val + val=$(openssl rand -hex 16 2>/dev/null) + local status=$? + if [ $status -ne 0 ] || [ ${#val} -ne 32 ]; then + echo "❌ Error: Failed to generate secure random UUID (openssl rand returned exit status $status, length ${#val})." >&2 + return 1 + fi + echo "${val:0:8}-${val:8:4}-${val:12:4}-${val:16:4}-${val:20:12}" +} + if [ -z "$NEXTAUTH_SECRET" ]; then NEXTAUTH_SECRET="$(openssl rand -base64 32)" echo "NEXTAUTH_SECRET=\"$NEXTAUTH_SECRET\"" >> "$ENV_FILE" @@ -171,6 +182,48 @@ if [ -z "$MINIO_ROOT_PASSWORD" ]; then echo "✓ Generated new MINIO_ROOT_PASSWORD and saved to $ENV_FILE" fi +if [ -z "$LANGFUSE_PUBLIC_KEY" ]; then + uuid=$(generate_uuid) + if [ $? -ne 0 ] || [ -z "$uuid" ]; then + echo "❌ Error: Failed to generate LANGFUSE_PUBLIC_KEY." >&2 + exit 1 + fi + LANGFUSE_PUBLIC_KEY="pk-lf-$uuid" + echo "LANGFUSE_PUBLIC_KEY=\"$LANGFUSE_PUBLIC_KEY\"" >> "$ENV_FILE" + chmod 600 "$ENV_FILE" + echo "✓ Generated new LANGFUSE_PUBLIC_KEY and saved to $ENV_FILE" +fi + +if [ -z "$LANGFUSE_SECRET_KEY" ]; then + uuid=$(generate_uuid) + if [ $? -ne 0 ] || [ -z "$uuid" ]; then + echo "❌ Error: Failed to generate LANGFUSE_SECRET_KEY." >&2 + exit 1 + fi + LANGFUSE_SECRET_KEY="sk-lf-$uuid" + echo "LANGFUSE_SECRET_KEY=\"$LANGFUSE_SECRET_KEY\"" >> "$ENV_FILE" + chmod 600 "$ENV_FILE" + echo "✓ Generated new LANGFUSE_SECRET_KEY and saved to $ENV_FILE" +fi + +if [ -z "$OLLAMA_API_KEY" ]; then + if [ -t 0 ]; then + echo "🔑 OLLAMA_API_KEY not found." + echo -n "Please enter your Ollama API Key (input will be hidden): " + read -rs OLLAMA_API_KEY + echo "" + echo "OLLAMA_API_KEY=\"$OLLAMA_API_KEY\"" >> "$ENV_FILE" + chmod 600 "$ENV_FILE" + echo "✓ Ollama API key saved securely to $ENV_FILE" + else + echo "❌ Error: OLLAMA_API_KEY is not set in your environment or in $ENV_FILE." + echo "Please run this script interactively first, or create the file manually:" + echo " echo 'OLLAMA_API_KEY=your_key_here' >> $ENV_FILE" + echo " chmod 600 $ENV_FILE" + exit 1 + fi +fi + # DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER in router config is resolved at runtime from env @@ -381,7 +434,7 @@ if podman pod exists agent-router-pod 2>/dev/null; then fi render_pod_yaml() { - export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD + export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY python3 - "$WORKDIR/pod.yaml" <<'PY' import os, sys, urllib.parse uid = os.getuid() @@ -391,12 +444,15 @@ placeholders = [ "/home/gpav/Vrac/LAB/AI/LLM-Routing", "/home/gpav/", "/run/user/1000", - "sk-lit...33bf", - "postgres:***", + "LITELLM_MASTER_KEY_PLACEHOLDER", + "POSTGRES_PASSWORD_RAW_PLACEHOLDER", + "POSTGRES_PASSWORD_ENCODED_PLACEHOLDER", "NEXTAUTH_SECRET_PLACEHOLDER", "SALT_PLACEHOLDER", "ENCRYPTION_KEY_PLACEHOLDER", - "postgres-password-***", + "OLLAMA_API_KEY_PLACEHOLDER", + "LANGFUSE_PUBLIC_KEY_PLACEHOLDER", + "LANGFUSE_SECRET_KEY_PLACEHOLDER", "MINIO_USER_PLACEHOLDER", "MINIO_PASSWORD_PLACEHOLDER", "LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER" @@ -408,14 +464,17 @@ for ph in placeholders: text = text.replace("/home/gpav/Vrac/LAB/AI/LLM-Routing", os.environ["WORKDIR"]) text = text.replace("/home/gpav/", os.environ["HOME"] + "/") text = text.replace("/run/user/1000", f"/run/user/{uid}") -text = text.replace("sk-lit...33bf", os.environ["LITELLM_MASTER_KEY"]) +text = text.replace("LITELLM_MASTER_KEY_PLACEHOLDER", os.environ["LITELLM_MASTER_KEY"]) +text = text.replace("POSTGRES_PASSWORD_RAW_PLACEHOLDER", os.environ["POSTGRES_PASSWORD"]) # URL-encode the postgres password for DSN insertion -encoded_password = urllib.parse.quote_plus(os.environ['POSTGRES_PASSWORD']) -text = text.replace("postgres:***", f"postgres:{encoded_password}") -text = text.replace("postgres-password-***", os.environ["POSTGRES_PASSWORD"]) +encoded_password = urllib.parse.quote(os.environ['POSTGRES_PASSWORD']) +text = text.replace("POSTGRES_PASSWORD_ENCODED_PLACEHOLDER", encoded_password) text = text.replace("NEXTAUTH_SECRET_PLACEHOLDER", os.environ["NEXTAUTH_SECRET"]) text = text.replace("SALT_PLACEHOLDER", os.environ["SALT"]) text = text.replace("ENCRYPTION_KEY_PLACEHOLDER", os.environ["ENCRYPTION_KEY"]) +text = text.replace("OLLAMA_API_KEY_PLACEHOLDER", os.environ["OLLAMA_API_KEY"]) +text = text.replace("LANGFUSE_PUBLIC_KEY_PLACEHOLDER", os.environ["LANGFUSE_PUBLIC_KEY"]) +text = text.replace("LANGFUSE_SECRET_KEY_PLACEHOLDER", os.environ["LANGFUSE_SECRET_KEY"]) text = text.replace("MINIO_USER_PLACEHOLDER", os.environ["MINIO_ROOT_USER"]) text = text.replace("MINIO_PASSWORD_PLACEHOLDER", os.environ["MINIO_ROOT_PASSWORD"]) text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", os.environ["LANGFUSE_INIT_USER_PASSWORD"]) From 660811f68f7aae8d096888a800b114bd0c40a2e5 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 09:32:33 +0200 Subject: [PATCH 09/22] chore: add branch audit plan and restore offload-aa-scores performance optimization --- .agents/branch_audit_plan.md | 56 ++++++++++++++++++++++++++ router/main.py | 5 ++- tests/test_compute_free_model_score.py | 4 ++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .agents/branch_audit_plan.md diff --git a/.agents/branch_audit_plan.md b/.agents/branch_audit_plan.md new file mode 100644 index 00000000..f1a9374c --- /dev/null +++ b/.agents/branch_audit_plan.md @@ -0,0 +1,56 @@ +# Git Merges & Regressions Audit Report + +## Problem Overview +During the merging of concurrent pull requests, automated conflict-resolution bots (`jules[bot]`) silently discarded or reverted several code blocks and logic optimizations. + +To ensure complete stability, we performed a systematic, automated three-dot diffing audit (`git diff CURRENT_BRANCH...TARGET_BRANCH`) comparing our current workspace against every active and merged remote branch in the repository. + +--- + +## Branch-by-Branch Audit Checklist + +| Branch Name / PR | Status | Notes / Discrepancies Checked | +| :--- | :---: | :--- | +| `origin/cleanup/remove-unused-get-live-gemini-oauth-token` (PR #195) | **CLEAN** | All changes present in current codebase. | +| `origin/perf-optimize-gemini-oauth-token-7488735575783948165` (PR #158) | **CLEAN** | All changes present in current codebase. | +| `origin/refactor-record-tool-usage-17779903941173107649` (PR #157) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/get-pr-status-tests-18330387033569416831` (PR #193) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/perf-async-annotations-4499495305587811104` (PR #172) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/add-gemini-oauth-tests-17524029845400706758` (PR #163) | **CLEAN** | All changes present in current codebase. | +| `origin/dependabot/docker/berriai/litellm-v1.90.2` (PR #192) | **CLEAN** | Diff showed `v1.90.2` LiteLLM image version bump, which is already applied. | +| `origin/security/fix-memory-mcp-category-injection-14795006529015952965` (PR #173) | **CLEAN** | All changes present in current codebase. | +| `origin/test/add-read-annotations-sync-tests-12043640228306609857` (PR #168) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/test-parse-key-parameterized-14651890987031951450` (PR #169) | **CLEAN** | All changes present in current codebase. | +| `origin/perf/async-quota-exhausted-4722057800691922723` (PR #156) | **CLEAN** | All changes present in current codebase. | +| `origin/perf-benchmark-classifier-14678555128983342452` (PR #155) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/add-test-redis-from-url-12746122485451637611` (PR #174) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/test-is-memory-key-14668590644102848561` (PR #164) | **CLEAN** | All changes present in current codebase. | +| `origin/⚡/optimize-agy-log-read-1519383298987985129` (PR #170) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/add-oauth-status-tests-15591368961301252072` (PR #166) | **CLEAN** | All changes present in current codebase. | +| `origin/chore/test-get-goose-sessions-16558526271462662412` (PR #165) | **CLEAN** | All changes present in current codebase. | +| `origin/fix-http-client-proliferation-11317864999360875839` | **CLEAN** | HTTP client unification is fully present in our workspace. | +| `origin/fix/test-record-tool-usage-3580129709200353766` | **CLEAN** | Root-level test files were reorganized to `/tests`, fully preserved. | +| `origin/fix/secrets-relocation` (PR #184) | **CLEAN** | Checked out placeholders and updated `start-stack.sh`/`pod.yaml` securely. | +| `origin/perf/offload-aa-scores-sync-16551127323385849872` (PR #98) | **RESTORED** | **Regression Found**: The asynchronous scores loading optimization was lost on master. Re-implemented and verified. | + +--- + +## Detailed Regression Analysis & Fix: Offload AA Scores Loading + +### The Regression +* **File:** `router/main.py` +* **Issue:** `_load_aa_scores()` (which does a blocking synchronous JSON disk read of `aa_scores.json`) was called directly inside `compute_free_model_score()`. When resolving or syncing OpenRouter free models, this synchronous file read was executed sequentially in a loop, blocking the main event loop. +* **Merged Fix Lost:** The merged PR #98 had offloaded this operation to a background thread pool using `await asyncio.to_thread(_load_aa_scores)` outside the loops in `sync_adaptive_router_roster` and `get_best_free_model`, but this change was completely discarded during automated conflict resolution. + +### The Resolution +1. **`router/main.py`**: + * Restored `await asyncio.to_thread(_load_aa_scores)` inside `sync_adaptive_router_roster()` and `get_best_free_model()` if the cache is not yet loaded. + * Removed the blocking `_load_aa_scores()` call from inside `compute_free_model_score()`. +2. **`tests/test_compute_free_model_score.py`**: + * Updated the test cases to explicitly call `router_main._load_aa_scores()` within the test setups to align with the asynchronous offloading. + +--- + +## Verification Status +* **Test Suite:** Ran `PYTHONPATH=router:. pytest` +* **Result:** **176 tests passed** successfully (100% success rate). diff --git a/router/main.py b/router/main.py index 9e379255..1f13d37a 100644 --- a/router/main.py +++ b/router/main.py @@ -530,6 +530,8 @@ async def sync_adaptive_router_roster(master_key: str): except Exception as e: logger.warning(f"Failed to fetch OpenRouter models: {e}") return + if not _AA_SCORES_LOADED: + await asyncio.to_thread(_load_aa_scores) free_models = [] model_contexts = {} model_supported_params = {} @@ -1450,7 +1452,6 @@ def _load_aa_scores(): def compute_free_model_score(m: dict) -> float: """Return AA agentic index score, or a low default for unknown models.""" - _load_aa_scores() mid = m.get("id", "") return _AA_SCORES_CACHE.get(mid, 25.0) @@ -1486,6 +1487,8 @@ async def _save_best_model_to_disk(best_model: dict) -> None: async def get_best_free_model() -> dict: """Fetches currently free models from OpenRouter, matches against agentic scores, and returns the highest.""" global free_model_cache + if not _AA_SCORES_LOADED: + await asyncio.to_thread(_load_aa_scores) now = time.time() # Check if cache is still valid diff --git a/tests/test_compute_free_model_score.py b/tests/test_compute_free_model_score.py index 2fab732c..1ea59751 100644 --- a/tests/test_compute_free_model_score.py +++ b/tests/test_compute_free_model_score.py @@ -18,6 +18,7 @@ def test_compute_free_model_score_known_model(): """Test when the model id exists in the cache.""" mock_data = json.dumps({"scores": {"model-a": 85.5}}) with patch("builtins.open", mock_open(read_data=mock_data)): + router_main._load_aa_scores() score = compute_free_model_score({"id": "model-a"}) assert score == 85.5 @@ -25,6 +26,7 @@ def test_compute_free_model_score_unknown_model(): """Test when the model id is not in the cache.""" mock_data = json.dumps({"scores": {"model-a": 85.5}}) with patch("builtins.open", mock_open(read_data=mock_data)): + router_main._load_aa_scores() score = compute_free_model_score({"id": "model-b"}) assert score == 25.0 @@ -32,12 +34,14 @@ def test_compute_free_model_score_missing_id(): """Test when the model dictionary is missing an 'id'.""" mock_data = json.dumps({"scores": {"model-a": 85.5}}) with patch("builtins.open", mock_open(read_data=mock_data)): + router_main._load_aa_scores() score = compute_free_model_score({"name": "just a name"}) assert score == 25.0 def test_compute_free_model_score_file_not_found(): """Test fallback when the aa_scores.json file is missing or fails to load.""" with patch("builtins.open", side_effect=FileNotFoundError): + router_main._load_aa_scores() score = compute_free_model_score({"id": "model-a"}) assert score == 25.0 assert router_main._AA_SCORES_LOADED is True From ad447547c919b179d4a5b364f94b013902e33a01 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 09:39:19 +0200 Subject: [PATCH 10/22] chore: address CodeRabbit review feedback on PR #198 --- router/main.py | 1 + router/memory_mcp.py | 14 ++++++--- router/tests/test_get_redis.py | 3 +- router/tests/test_load_persisted_stats.py | 3 ++ router/tests/test_memory_mcp.py | 37 +++++++++++++++++------ scripts/benchmark_classifier.py | 2 +- scripts/benchmark_tokens.py | 5 +-- 7 files changed, 47 insertions(+), 18 deletions(-) diff --git a/router/main.py b/router/main.py index 1f13d37a..036cfee2 100644 --- a/router/main.py +++ b/router/main.py @@ -3965,6 +3965,7 @@ async def save_annotations(payload: AnnotationPayload): else: existing[k] = item.model_dump() await _atomic_write_json_async(str(ann_path), existing) + _annotations_cache.pop(str(ann_path), None) return JSONResponse({"status": "ok", "saved": len(data)}) except Exception as e: diff --git a/router/memory_mcp.py b/router/memory_mcp.py index 7ec87c26..326ad66a 100755 --- a/router/memory_mcp.py +++ b/router/memory_mcp.py @@ -45,17 +45,21 @@ def _make_key(category: str, is_global: bool, data: str) -> str: # BLAKE2b: SOTA crypto hash, stdlib, faster than MD5, deterministic across restarts. # Provides uniqueness within the same millisecond. h = hashlib.blake2b((data + str(ts)).encode("utf-8"), digest_size=HASH_DIGEST_SIZE).hexdigest() - safe_category = urllib.parse.quote(category) - return f"{PREFIX}:{scope}:{safe_category}::{ts}:{h}" + safe_category = urllib.parse.quote(category, safe="") + return f"{PREFIX}:v2:{scope}:{safe_category}::{ts}:{h}" def _parse_key(key: str): """Parse a structured key back into (scope, category, timestamp, hash).""" try: parts = key.split("::") - prefix = parts[0].split(":") # memory:{scope}:{category} - scope = prefix[1] if len(prefix) > 1 else "" - category = urllib.parse.unquote(prefix[2]) if len(prefix) > 2 else "" + prefix = parts[0].split(":") # memory:{scope}:{category} or memory:v2:{scope}:{category} + if len(prefix) > 1 and prefix[1] == "v2": + scope = prefix[2] if len(prefix) > 2 else "" + category = urllib.parse.unquote(prefix[3]) if len(prefix) > 3 else "" + else: + scope = prefix[1] if len(prefix) > 1 else "" + category = prefix[2] if len(prefix) > 2 else "" ts_hash = parts[1] if len(parts) > 1 else "" ts = ts_hash.split(":")[0] if ts_hash else "" return {"scope": scope, "category": category, "timestamp": ts} diff --git a/router/tests/test_get_redis.py b/router/tests/test_get_redis.py index 0fdeaed2..10070ed5 100644 --- a/router/tests/test_get_redis.py +++ b/router/tests/test_get_redis.py @@ -7,8 +7,9 @@ import router.main as main @pytest.fixture(autouse=True) -def reset_redis_globals(): +def reset_redis_globals(monkeypatch): """Reset the global variables before and after each test.""" + monkeypatch.delenv("VALKEY_URL", raising=False) original_client = main._redis_client original_last_attempt = main._redis_last_init_attempt diff --git a/router/tests/test_load_persisted_stats.py b/router/tests/test_load_persisted_stats.py index 76edbf51..ea0f9ad2 100644 --- a/router/tests/test_load_persisted_stats.py +++ b/router/tests/test_load_persisted_stats.py @@ -1,3 +1,6 @@ +import os +os.environ.setdefault("CONFIG_PATH", "/config/router_dir/config.yaml") + import json import pytest from unittest.mock import patch, mock_open diff --git a/router/tests/test_memory_mcp.py b/router/tests/test_memory_mcp.py index 8c93f1f6..f0e6a6b8 100644 --- a/router/tests/test_memory_mcp.py +++ b/router/tests/test_memory_mcp.py @@ -37,11 +37,11 @@ def test_make_key_global(): key = _make_key(category, True, data) after_ts = int(time.time() * 1000) - # Expected format: f"{PREFIX}:{scope}:{category}::{ts}:{h}" - assert key.startswith(f"{PREFIX}:{SCOPE_GLOBAL}:{category}::") + # Expected format: f"{PREFIX}:v2:{scope}:{category}::{ts}:{h}" + assert key.startswith(f"{PREFIX}:v2:{SCOPE_GLOBAL}:{category}::") # Extract timestamp and hash part - match = re.match(rf"^{PREFIX}:{SCOPE_GLOBAL}:{category}::(\d+):([a-f0-9]+)$", key) + match = re.match(rf"^{PREFIX}:v2:{SCOPE_GLOBAL}:{category}::(\d+):([a-f0-9]+)$", key) assert match is not None, f"Key {key} does not match expected format" ts = int(match.group(1)) @@ -60,9 +60,9 @@ def test_make_key_local(): key = _make_key(category, False, data) after_ts = int(time.time() * 1000) - assert key.startswith(f"{PREFIX}:{SCOPE_LOCAL}:{category}::") + assert key.startswith(f"{PREFIX}:v2:{SCOPE_LOCAL}:{category}::") - match = re.match(rf"^{PREFIX}:{SCOPE_LOCAL}:{category}::(\d+):([a-f0-9]+)$", key) + match = re.match(rf"^{PREFIX}:v2:{SCOPE_LOCAL}:{category}::(\d+):([a-f0-9]+)$", key) assert match is not None, f"Key {key} does not match expected format" ts = int(match.group(1)) @@ -79,10 +79,10 @@ def test_make_key_formatting_details(monkeypatch): # data="data", ts=1620000000123 -> blake2b("data1620000000123", digest_size=10) -> 5e5dad075ca7764bc51f key1 = _make_key("cat1", True, "data") - assert key1 == f"{PREFIX}:{SCOPE_GLOBAL}:cat1::1620000000123:5e5dad075ca7764bc51f" + assert key1 == f"{PREFIX}:v2:{SCOPE_GLOBAL}:cat1::1620000000123:5e5dad075ca7764bc51f" key2 = _make_key("cat2", False, "data") - assert key2 == f"{PREFIX}:{SCOPE_LOCAL}:cat2::1620000000123:5e5dad075ca7764bc51f" + assert key2 == f"{PREFIX}:v2:{SCOPE_LOCAL}:cat2::1620000000123:5e5dad075ca7764bc51f" def test_make_key_determinism_and_uniqueness(): @@ -272,6 +272,10 @@ def test_memory_entry_missing_fields(): "memory:global:general::20240101T120000Z", {"scope": "global", "category": "general", "timestamp": "20240101T120000Z"}, ), + ( + "memory:v2:local:proj%3Aalpha%2F100%25%20ready::20240101T120000Z:abc123hash", + {"scope": "local", "category": "proj:alpha/100% ready", "timestamp": "20240101T120000Z"}, + ), ], ids=[ "happy_path", @@ -282,6 +286,7 @@ def test_memory_entry_missing_fields(): "invalid_type", "extra_colons_in_category", "missing_hash_but_has_timestamp", + "v2_escaped_category", ] ) def test_parse_key(key, expected): @@ -295,13 +300,13 @@ def test_parse_memory_value_valid_json(): assert result == {"data": "some data", "tags": ["tag1", "tag2"]} -def test_parse_memory_value_invalid_json(): +def test_parse_memory_value_invalid_json_fallback(): raw_data = "this is not json" result = _parse_memory_value(raw_data) assert result == {"data": "this is not json", "tags": []} -def test_parse_memory_value_type_error(): +def test_parse_memory_value_type_error_fallback(): raw_data = 12345 result = _parse_memory_value(raw_data) # type: ignore[arg-type] assert result == {"data": 12345, "tags": []} @@ -311,3 +316,17 @@ def test_parse_memory_value_non_dict_json(): raw_data = '"just a string"' result = _parse_memory_value(raw_data) assert result == "just a string" + + +def test_make_key_and_parse_key_round_trip(): + """Verify that _make_key and _parse_key correctly quote and unquote complex categories.""" + category = "proj:alpha/100% ready" + key = _make_key(category, is_global=False, data="test-data") + + # Assert that the category in the key is URL-encoded + assert "proj%3Aalpha%2F100%25%20ready" in key + + # Assert that the parsed key returns the original unencoded category + parsed = _parse_key(key) + assert parsed["scope"] == "local" + assert parsed["category"] == category diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py index 9ff795b4..8e5f0766 100644 --- a/scripts/benchmark_classifier.py +++ b/scripts/benchmark_classifier.py @@ -59,7 +59,7 @@ def classify(prompt): def process_item(item): prompt = item["prompt"] - expected = item.get("tier") or item.get("llm_tier") or item.get("clf_tier", "") + expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") try: predicted = classify(prompt) diff --git a/scripts/benchmark_tokens.py b/scripts/benchmark_tokens.py index d701b4eb..1fe8da63 100644 --- a/scripts/benchmark_tokens.py +++ b/scripts/benchmark_tokens.py @@ -65,12 +65,13 @@ def calculate_factorial(n): print(f" --> FAILURE: {case['name']} error exceeds target threshold") all_passed = False - assert all_passed, "Token estimation accuracy benchmark failed" + if not all_passed: + raise ValueError("Token estimation accuracy benchmark failed") if __name__ == "__main__": try: verify_accuracy() sys.exit(0) - except AssertionError as e: + except ValueError as e: print(f"\nERROR: {e}") sys.exit(1) From b0a8605600e42eb1f89c04220755252ba235b840 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 10:09:09 +0200 Subject: [PATCH 11/22] perf: optimize async log checking in agy_proxy to seek and read 1KB --- router/agy_proxy.py | 18 ++++++++++++------ router/tests/test_agy_proxy.py | 14 +++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/router/agy_proxy.py b/router/agy_proxy.py index 8b77ac18..4e911c42 100644 --- a/router/agy_proxy.py +++ b/router/agy_proxy.py @@ -152,12 +152,18 @@ async def _is_quota_exhausted(returncode: int, stdout: str, stderr: str) -> bool _last_log_check = now log_path = os.path.expanduser("~/.gemini/antigravity-cli/cli.log") try: - if os.path.exists(log_path): - async with aiofiles.open(log_path, "r") as f: - lines = await f.readlines() - for line in lines[-5:]: - if "RESOURCE_EXHAUSTED" in line or "code 429" in line: - return True + async with aiofiles.open(log_path, "rb") as f: + try: + await f.seek(0, 2) + file_size = await f.tell() + await f.seek(max(0, file_size - 1024)) + except OSError: + pass + content_bytes = await f.read() + content = content_bytes.decode("utf-8", errors="ignore") + for line in content.splitlines()[-5:]: + if "RESOURCE_EXHAUSTED" in line or "code 429" in line: + return True except Exception: pass # Empty stdout+stderr with rc=0 strongly suggests quota exhaustion diff --git a/router/tests/test_agy_proxy.py b/router/tests/test_agy_proxy.py index f4cd540d..8ca6ee3b 100644 --- a/router/tests/test_agy_proxy.py +++ b/router/tests/test_agy_proxy.py @@ -74,16 +74,16 @@ async def test_is_quota_exhausted_success(): async def test_is_quota_exhausted_other_error(): assert await _is_quota_exhausted(1, "", "some other random error") is False -@patch("router.agy_proxy.os.path.exists") @patch("aiofiles.open") @patch("router.agy_proxy.time.time") @pytest.mark.asyncio -async def test_is_quota_exhausted_empty_reads_log(mock_time, mock_open, mock_exists): +async def test_is_quota_exhausted_empty_reads_log(mock_time, mock_open): mock_time.return_value = 1000.0 - mock_exists.return_value = True mock_file = AsyncMock() - mock_file.readlines.return_value = ["line 1\n", "RESOURCE_EXHAUSTED info\n", "line 3\n"] + mock_file.seek = AsyncMock() + mock_file.tell = AsyncMock(return_value=100) + mock_file.read = AsyncMock(return_value=b"line 1\nRESOURCE_EXHAUSTED info\nline 3\n") mock_open.return_value.__aenter__.return_value = mock_file with patch("router.agy_proxy._last_log_check", 0): @@ -98,12 +98,12 @@ async def test_is_quota_exhausted_empty_throttled(mock_time): # Even without reading log, falls back to True assert await _is_quota_exhausted(0, "", "") is True -@patch("router.agy_proxy.os.path.exists") +@patch("aiofiles.open") @patch("router.agy_proxy.time.time") @pytest.mark.asyncio -async def test_is_quota_exhausted_empty_no_log_fallback(mock_time, mock_exists): +async def test_is_quota_exhausted_empty_no_log_fallback(mock_time, mock_open): mock_time.return_value = 1000.0 - mock_exists.return_value = False + mock_open.side_effect = FileNotFoundError() with patch("router.agy_proxy._last_log_check", 0): assert await _is_quota_exhausted(0, "", "") is True From 186c9d43bff2c13750271054802514a289e24b4f Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 11:09:47 +0200 Subject: [PATCH 12/22] chore: address new CodeRabbit and Gemini Code Assist reviews --- .agents/jules_regression_analysis.md | 2 +- README.md | 2 +- litellm/entrypoint.py | 6 ++++-- router/main.py | 2 +- router/tests/test_memory_mcp.py | 9 +++++++++ scripts/benchmark_classifier.py | 19 ++++++++----------- start-stack.sh | 16 +++++++--------- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.agents/jules_regression_analysis.md b/.agents/jules_regression_analysis.md index 78899f39..37b17229 100644 --- a/.agents/jules_regression_analysis.md +++ b/.agents/jules_regression_analysis.md @@ -61,7 +61,7 @@ To prevent automated agents from causing directory and code regressions, the fol Add a post-conflict verification script step to the agent workflow: ```bash # Verify no files were moved to root unexpectedly -git status | grep -E "renamed:|deleted:|new file:" +git status --porcelain ``` --- diff --git a/README.md b/README.md index 7c80825c..8b22f7f3 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ Exposes the entry endpoint (`http://localhost:5000/v1`) and evaluates prompt com > Model capabilities, token limits, and costs are visible in LiteLLM's Model Hub Table at `http://localhost:4000/ui/?page=model-hub-table` (or port 4000 on the gateway host). ### B. LiteLLM Proxy Gateway (`litellm/config.yaml`) -- **Version Pinning**: The LiteLLM gateway runs `ghcr.io/berriai/litellm:v1.90.2` (latest stable as of June 2026). The tag is explicitly pinned in `pod.yaml` — never use `:latest`. Check available tags with `skopeo list-tags docker://ghcr.io/berriai/litellm` before upgrading. ClickHouse runs `docker.io/clickhouse/clickhouse-server:26.5.1` (upgraded from 24.8, June 2026). Valkey Cache runs `docker.io/valkey/valkey:9.1.0-alpine` (upgraded from 8, June 2026). +- **Version Pinning**: The LiteLLM gateway, ClickHouse, and Valkey Cache image tags are explicitly pinned in `pod.yaml` — never use `:latest`. Check available tags with `skopeo list-tags` or registry hubs before upgrading. Orchestrates routing fallback chains, Redis caching, and telemetry callbacks: - **`drop_params: true`**: Automatically strips unsupported arguments when transitioning to models that don't support them. - **Request Timeouts (`300s`)**: Provides ample padding to prevent connection aborts during dynamic RAM swapping operations on the local GPU `llama-server`. diff --git a/litellm/entrypoint.py b/litellm/entrypoint.py index 20d4baf0..df7a2ec8 100644 --- a/litellm/entrypoint.py +++ b/litellm/entrypoint.py @@ -106,6 +106,10 @@ def strptime(cls, date_str: str, fmt: str) -> original_datetime: # instances, which no longer match. We must register both classes. try: from prisma.builder import serializer +except ImportError: + serializer = None + +if serializer is not None: def _serialize_dt(dt): """Serialize datetime to ISO8601 with timezone (UTC if naive).""" if dt.tzinfo is None: @@ -114,8 +118,6 @@ def _serialize_dt(dt): serializer.register(original_datetime, _serialize_dt) serializer.register(RobustDatetime, _serialize_dt) print("🩹 Registered original_datetime + RobustDatetime with Prisma serializer") -except Exception: - pass sys.stdout.flush() # Start LiteLLM Proxy diff --git a/router/main.py b/router/main.py index 036cfee2..d9292f99 100644 --- a/router/main.py +++ b/router/main.py @@ -142,7 +142,7 @@ def estimate_prompt_tokens(body: dict) -> int: # Include a flat estimate for system prompt / metadata overhead. # Use rounding to avoid truncation bias (e.g., 1.9 -> 1). - return max(1, int(round(total)) + 50) + return max(1, round(total) + 50) async def sync_cooldowns_from_valkey() -> None: diff --git a/router/tests/test_memory_mcp.py b/router/tests/test_memory_mcp.py index f0e6a6b8..236853eb 100644 --- a/router/tests/test_memory_mcp.py +++ b/router/tests/test_memory_mcp.py @@ -16,6 +16,7 @@ PREFIX, SCOPE_GLOBAL, SCOPE_LOCAL, + _is_memory_key, _make_key, _memory_entry, _memory_value, @@ -236,6 +237,14 @@ def test_memory_entry_missing_fields(): result3 = _memory_entry({}) assert result3 is None +def test_is_memory_key_types(): + """Test _is_memory_key works with both string and non-string inputs.""" + assert _is_memory_key("memory:local:test") is True + assert _is_memory_key("other:prefix") is False + assert _is_memory_key(None) is False + assert _is_memory_key(12345) is False + assert _is_memory_key([]) is False + @pytest.mark.parametrize( "key, expected", diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py index 8e5f0766..8000fa5e 100644 --- a/scripts/benchmark_classifier.py +++ b/scripts/benchmark_classifier.py @@ -1,6 +1,8 @@ """Benchmark gemma4-26a4b-routing classifier against labeled dataset.""" import os import json, urllib.request, time, sys +import concurrent.futures +import threading from collections import defaultdict, Counter from pathlib import Path @@ -54,9 +56,6 @@ def classify(prompt): per_tier = {t: {"correct": 0, "total": 0} for t in TIERS} confusion = defaultdict(Counter) # confusion[expected][predicted] -import concurrent.futures -import threading - def process_item(item): prompt = item["prompt"] expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") @@ -71,14 +70,14 @@ def process_item(item): results_list = [None] * total with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor: - rate_limit_lock = threading.Lock() + sem = threading.Semaphore(5) def process_item_with_rate_limit(index_and_item): i, item = index_and_item - with rate_limit_lock: + with sem: + # Add a small delay between acquisitions to stagger server load time.sleep(0.05) - - expected, predicted = process_item(item) + expected, predicted = process_item(item) return i, item, expected, predicted futures = [executor.submit(process_item_with_rate_limit, (i, item)) for i, item in enumerate(dataset.get("prompts", []))] @@ -93,14 +92,12 @@ def process_item_with_rate_limit(index_and_item): "predicted": predicted, } - if expected not in per_tier: - confusion[expected][predicted] += 1 - else: + if expected in per_tier: per_tier[expected]["total"] += 1 if predicted == expected: correct += 1 per_tier[expected]["correct"] += 1 - confusion[expected][predicted] += 1 + confusion[expected][predicted] += 1 completed_count += 1 diff --git a/start-stack.sh b/start-stack.sh index 1f751823..e69929ea 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -31,7 +31,12 @@ if [ -f "$ENV_FILE" ]; then fi # Ensure openssl is installed if we need to generate passwords/keys - +if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ] || [ -z "$MINIO_ROOT_USER" ] || [ -z "$MINIO_ROOT_PASSWORD" ] || [ -z "$LANGFUSE_INIT_USER_PASSWORD" ] || [ -z "$REDIS_AUTH" ] || [ -z "$CLICKHOUSE_PASSWORD" ] || [ -z "$LANGFUSE_PUBLIC_KEY" ] || [ -z "$LANGFUSE_SECRET_KEY" ]; then + if ! command -v openssl &>/dev/null; then + echo "❌ Error: 'openssl' is required to generate secure random keys but was not found in PATH." + exit 1 + fi +fi if [ -z "$OPENROUTER_API_KEY" ]; then if [ -t 0 ]; then @@ -95,13 +100,6 @@ else echo "⚠️ Warning: Host agy daemon not responding on port 5005" fi -if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$NEXTAUTH_SECRET" ] || [ -z "$SALT" ] || [ -z "$ENCRYPTION_KEY" ] || [ -z "$LITELLM_MASTER_KEY" ] || [ -z "$ROUTER_API_KEY" ] || [ -z "$MINIO_ROOT_USER" ] || [ -z "$MINIO_ROOT_PASSWORD" ] || [ -z "$LANGFUSE_INIT_USER_PASSWORD" ] || [ -z "$REDIS_AUTH" ] || [ -z "$CLICKHOUSE_PASSWORD" ] || [ -z "$LANGFUSE_PUBLIC_KEY" ] || [ -z "$LANGFUSE_SECRET_KEY" ]; then - if ! command -v openssl &>/dev/null; then - echo "❌ Error: 'openssl' is required to generate secure random keys but was not found in PATH." - exit 1 - fi -fi - # Ensure the env file exists and has secure permissions (owner read/write only) touch "$ENV_FILE" chmod 600 "$ENV_FILE" @@ -467,7 +465,7 @@ text = text.replace("/run/user/1000", f"/run/user/{uid}") text = text.replace("LITELLM_MASTER_KEY_PLACEHOLDER", os.environ["LITELLM_MASTER_KEY"]) text = text.replace("POSTGRES_PASSWORD_RAW_PLACEHOLDER", os.environ["POSTGRES_PASSWORD"]) # URL-encode the postgres password for DSN insertion -encoded_password = urllib.parse.quote(os.environ['POSTGRES_PASSWORD']) +encoded_password = urllib.parse.quote(os.environ['POSTGRES_PASSWORD'], safe="") text = text.replace("POSTGRES_PASSWORD_ENCODED_PLACEHOLDER", encoded_password) text = text.replace("NEXTAUTH_SECRET_PLACEHOLDER", os.environ["NEXTAUTH_SECRET"]) text = text.replace("SALT_PLACEHOLDER", os.environ["SALT"]) From e7792afd30dc6d7e0ed9a63ec48ec495a93a9ec1 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 11:21:49 +0200 Subject: [PATCH 13/22] fix(chore): resolve broken script paths, sys.path imports and relative quota reset paths --- scripts/verification/verify_breaker.py | 2 +- scripts/watch_quota.sh | 3 ++- start-stack.sh | 2 +- tests/test_a2_verify.py | 2 +- tests/test_circuit_breaker.py | 2 +- tests/test_models_proxy.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/verification/verify_breaker.py b/scripts/verification/verify_breaker.py index daf41bef..6ed9e7c7 100644 --- a/scripts/verification/verify_breaker.py +++ b/scripts/verification/verify_breaker.py @@ -3,7 +3,7 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) from router.circuit_breaker import get_breaker diff --git a/scripts/watch_quota.sh b/scripts/watch_quota.sh index 8bf9d090..cb95debd 100755 --- a/scripts/watch_quota.sh +++ b/scripts/watch_quota.sh @@ -2,7 +2,8 @@ # Polling loop — checks quota every 30s and runs tests when reset # Log file to watch LOG_FILE="$HOME/.gemini/antigravity-cli/cli.log" -TEST_SCRIPT="test_quota_reset.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEST_SCRIPT="$SCRIPT_DIR/test_quota_reset.sh" POLL_INTERVAL=30 # seconds echo "=== Quota Reset Watcher ===" diff --git a/start-stack.sh b/start-stack.sh index e69929ea..28808894 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -73,7 +73,7 @@ if [ -f "$OAUTH_CREDS" ]; then fi fi if $NEED_SYNC; then - python3 sync_gemini_token.py || echo "⚠️ Warning: Failed to sync Gemini token from keyring" + python3 scripts/sync_gemini_token.py || echo "⚠️ Warning: Failed to sync Gemini token from keyring" fi ACTIVE_OAUTH="" diff --git a/tests/test_a2_verify.py b/tests/test_a2_verify.py index 42cde1e1..d2f0be9a 100644 --- a/tests/test_a2_verify.py +++ b/tests/test_a2_verify.py @@ -2,7 +2,7 @@ """Verify circuit breaker integration into agy_proxy.py""" import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent / 'router')) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'router')) from circuit_breaker import get_breaker from agy_proxy import try_agy_proxy diff --git a/tests/test_circuit_breaker.py b/tests/test_circuit_breaker.py index 8cdcad60..e4570c2a 100644 --- a/tests/test_circuit_breaker.py +++ b/tests/test_circuit_breaker.py @@ -19,7 +19,7 @@ import pytest from unittest.mock import patch, AsyncMock from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from router.circuit_breaker import get_breaker, TIER_COOLDOWNS, MAX_TIER diff --git a/tests/test_models_proxy.py b/tests/test_models_proxy.py index bfaf3b81..c325a39b 100644 --- a/tests/test_models_proxy.py +++ b/tests/test_models_proxy.py @@ -9,7 +9,7 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent / "router")) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "router")) from main import get_http_client, proxy_models, HTTP_MAX_CONNECTIONS, HTTP_MAX_KEEPALIVE_CONNECTIONS, HTTP_KEEPALIVE_EXPIRY From ba7deff8eae77882eeb1129b931a9af85354a879 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 11:43:02 +0200 Subject: [PATCH 14/22] fix(triage): truncate classifier prompt to 300 chars to avoid cache-ram limits and add early arg parsing for --help in start-stack.sh --- router/main.py | 4 +++- start-stack.sh | 31 ++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/router/main.py b/router/main.py index d9292f99..eeff350e 100644 --- a/router/main.py +++ b/router/main.py @@ -980,9 +980,11 @@ async def classify_request( try: client = get_http_client() + # Truncate prompt to prevent 400 Bad Request in llama-server due to cache-ram limit + truncated_prompt = prompt[:300] if len(prompt) > 300 else prompt payload = { "model": router_model_name, - "messages": [{"role": "user", "content": system_prompt + prompt}], + "messages": [{"role": "user", "content": system_prompt + truncated_prompt}], "temperature": 0.0, "max_tokens": 15, } diff --git a/start-stack.sh b/start-stack.sh index 28808894..20615f54 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -12,6 +12,29 @@ set -e WORKDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$WORKDIR" +show_help() { + echo "Usage:" + echo " ./start-stack.sh → Restart existing pod (fast, preserves logs)" + echo " ./start-stack.sh --replace → Stop, clean up zombie ports, and recreate/redeploy pod" + echo " ./start-stack.sh --full-rebuild → Rebuild router image and recreate/redeploy pod" + echo " ./start-stack.sh --help | -h → Show this help message and exit" + exit 0 +} + +FULL_REBUILD=false +REPLACE_MODE=false +if [ "${1:-}" = "--full-rebuild" ]; then + FULL_REBUILD=true +elif [ "${1:-}" = "--replace" ]; then + REPLACE_MODE=true +elif [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then + show_help +elif [ -n "${1:-}" ]; then + echo "❌ Error: Unknown argument '${1}'" + show_help + exit 1 +fi + # Ensure local volume directories exist on the host for Podman mounts mkdir -p valkey-data postgres-data langfuse-data clickhouse-data redis-lf-data minio-data @@ -226,13 +249,7 @@ fi # DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER in router config is resolved at runtime from env -FULL_REBUILD=false -REPLACE_MODE=false -if [ "${1:-}" = "--full-rebuild" ]; then - FULL_REBUILD=true -elif [ "${1:-}" = "--replace" ]; then - REPLACE_MODE=true -fi +# Arguments parsed at top of script # ── Cleanup zombie host-network ports ── # Podman with host networking can leave stuck LISTEN sockets after SIGKILL. From 9314a5e532661e402e9665fbdd66d08162063e4c Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 11:59:04 +0200 Subject: [PATCH 15/22] fix(triage): map virtual target_model llm-routing-agy to agent-advanced-core during fallback --- router/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/router/main.py b/router/main.py index eeff350e..67c87893 100644 --- a/router/main.py +++ b/router/main.py @@ -2170,6 +2170,8 @@ async def agy_stream_generator(): pass logger.error(f"agy proxy failed: {type(e).__name__}, falling back to LiteLLM") + if target_model == "llm-routing-agy": + target_model = "agent-advanced-core" original_target_model = target_model # --- OLLAMA (via LiteLLM) --- From d45b9518e19554b5794cb5eee2b437c3ab93e4b7 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 12:07:38 +0200 Subject: [PATCH 16/22] docs: add section 9d documenting live stack tier testing script and save test_reasoning_tiers.py --- README.md | 26 +++++- scripts/verification/test_reasoning_tiers.py | 87 ++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100755 scripts/verification/test_reasoning_tiers.py diff --git a/README.md b/README.md index 8b22f7f3..bce2a245 100644 --- a/README.md +++ b/README.md @@ -790,7 +790,31 @@ The cooldown mechanism works as follows: For auto-routing modes, the Triage Router handles failures by silently falling back to the classified free tier cascade. For direct requests to `llm-routing-ollama`, the router returns `429` immediately during cooldown, allowing LiteLLM to skip this model group and cascade to `openrouter-auto`. The cooldown status is visible via the `/metrics` endpoint (`ollama_cooldown_active` and `ollama_cooldown_remaining_seconds` gauges). -## 10. Performance Benchmarks\n\nThrough our local benchmarks, the following performance characteristics have been achieved: +## 9d. Live Stack Tier Testing & Verification + +The repository includes an automated integration script to test the 5-tier intent routing pipeline on the live gateway stack: +* **Location**: [test_reasoning_tiers.py](file:///home/gpav/Vrac/LAB/AI/LLM-Routing/scripts/verification/test_reasoning_tiers.py) + +This script sends five sequential chat completion requests (from simple to advanced prompt complexities) to the gateway's `llm-routing-auto-free` auto-triage route, verifying that: +1. The local classifier correctly categorizes and labels the prompt intent. +2. The gateway successfully routes the prompt to the mapped LiteLLM fallback model group or provider. +3. The responses are returned successfully with acceptable latency. + +### How to Run + +Ensure the container stack is deployed and healthy: +```bash +./start-stack.sh +``` + +Execute the verification script: +```bash +./scripts/verification/test_reasoning_tiers.py +``` + +## 10. Performance Benchmarks + +Through our local benchmarks, the following performance characteristics have been achieved: | Triage Evaluation Layer | Latency Footprint | Hardware Offload | Efficiency Ratio | | :--- | :---: | :---: | :---: | diff --git a/scripts/verification/test_reasoning_tiers.py b/scripts/verification/test_reasoning_tiers.py new file mode 100755 index 00000000..6f702300 --- /dev/null +++ b/scripts/verification/test_reasoning_tiers.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Verification script to test the 5-tier intent classification and routing pipeline +of the LLM-Routing gateway. + +This script sends varying prompt complexities to trigger classification across +all 5 triage levels and validates the gateway's routing responses. +""" + +import httpx +import json +import time + +gateway_url = "http://127.0.0.1:5000/v1/chat/completions" +headers = { + "Authorization": "Bearer gateway-pass", + "Content-Type": "application/json" +} + +prompts = [ + { + "name": "Simple (1/5)", + "prompt": "Write a one-line hello world in Python." + }, + { + "name": "Medium (2/5)", + "prompt": "Refactor this Python function to add a docstring and use a type hint: def sum(a, b): return a + b" + }, + { + "name": "Complex (3/5)", + "prompt": "Write a multi-file Python script implementing a complete ETL data pipeline that parses a complex nested JSON file of users and logs, validates the data with Pydantic, writes it to a PostgreSQL database using asyncpg, handles connection pool retries with backoff, and includes unit tests with pytest mocks." + }, + { + "name": "Reasoning (4/5)", + "prompt": "Compare the design trade-offs of using Valkey/Redis Sentinel versus Valkey/Redis Cluster for high availability in a high-throughput, low-latency microservice architecture. Analyze failure detection mechanisms, failover times, read/write scaling, client complexity, and network partition (split-brain) scenarios." + }, + { + "name": "Advanced (5/5)", + "prompt": "Design a highly secure, zero-trust microservice topology for a federated agent-routing system deployed across multiple Kubernetes clusters in different cloud regions. Incorporate mutual TLS via Istio service mesh, automated certificate rotation with cert-manager, OIDC/OAuth2 authentication via an external identity provider, centralized audit logging/distributed tracing using Langfuse and Clickhouse, regional failover routing based on latency, and DDoS protection." + } +] + +def run_tests(): + print("🚀 Starting 5-tier reasoning test queries...") + print("=" * 60) + for p in prompts: + print(f"\n📂 Sending {p['name']} Prompt:") + print(f" Prompt: {p['prompt'][:100]}...") + + payload = { + "model": "llm-routing-auto-free", + "messages": [ + {"role": "user", "content": p["prompt"]} + ], + "temperature": 0.0, + "max_tokens": 150 + } + + start_time = time.time() + try: + r = httpx.post(gateway_url, json=payload, headers=headers, timeout=120.0) + elapsed = time.time() - start_time + print(f" Status: {r.status_code}") + + if r.status_code == 200: + data = r.json() + model_used = data.get("model", "unknown") + try: + msg = data["choices"][0]["message"] + content = msg.get("content") + if content is not None: + content_clean = content.strip().replace("\n", " ") + else: + content_clean = "" + print(f" Routed Model: {model_used}") + print(f" Response Preview: {content_clean[:120]}...") + except Exception as parse_inner: + print(f" Failed to parse response choices: {parse_inner}") + print(f" Raw Data: {data}") + print(f" Latency: {elapsed:.2f}s") + else: + print(f" Error: {r.text}") + except Exception as e: + print(f" Request Exception: {e}") + +if __name__ == "__main__": + run_tests() From 1963807a3d8e0191e44abc5e1aeff3d88dd75608 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 12:13:18 +0200 Subject: [PATCH 17/22] feat(triage): make classifier prompt truncation limit configurable via CLASSIFIER_INPUT_MAX_CHARS env var --- router/main.py | 3 ++- start-stack.sh | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/router/main.py b/router/main.py index 67c87893..c26a7e40 100644 --- a/router/main.py +++ b/router/main.py @@ -981,7 +981,8 @@ async def classify_request( try: client = get_http_client() # Truncate prompt to prevent 400 Bad Request in llama-server due to cache-ram limit - truncated_prompt = prompt[:300] if len(prompt) > 300 else prompt + max_chars = int(os.getenv("CLASSIFIER_INPUT_MAX_CHARS", "300")) + truncated_prompt = prompt[:max_chars] if len(prompt) > max_chars else prompt payload = { "model": router_model_name, "messages": [{"role": "user", "content": system_prompt + truncated_prompt}], diff --git a/start-stack.sh b/start-stack.sh index 20615f54..4ad2fbcd 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -245,6 +245,13 @@ if [ -z "$OLLAMA_API_KEY" ]; then fi fi +if [ -z "$CLASSIFIER_INPUT_MAX_CHARS" ]; then + CLASSIFIER_INPUT_MAX_CHARS="300" + echo "CLASSIFIER_INPUT_MAX_CHARS=\"$CLASSIFIER_INPUT_MAX_CHARS\"" >> "$ENV_FILE" + echo "✓ Set default CLASSIFIER_INPUT_MAX_CHARS=300 and saved to $ENV_FILE" +fi + + # DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER in router config is resolved at runtime from env @@ -449,7 +456,7 @@ if podman pod exists agent-router-pod 2>/dev/null; then fi render_pod_yaml() { - export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY + export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY CLASSIFIER_INPUT_MAX_CHARS python3 - "$WORKDIR/pod.yaml" <<'PY' import os, sys, urllib.parse uid = os.getuid() From a08d453664c4114c3aa024099b203f22c892b408 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 15:19:26 +0200 Subject: [PATCH 18/22] chore: address PR review feedback and clean up test boilerplate --- .agents/branch_audit_plan.md | 2 +- .agents/jules_regression_analysis.md | 2 +- .github/workflows/test.yml | 2 +- pod.yaml | 16 +-- router/agy_proxy.py | 1 + router/memory_mcp.py | 15 ++- router/tests/test_get_gemini_oauth_status.py | 2 + router/tests/test_load_persisted_stats.py | 33 +++--- router/tests/test_memory_mcp.py | 2 +- scripts/benchmark_classifier.py | 10 +- start-stack.sh | 21 ++-- tests/conftest.py | 16 +++ tests/test_a2_verify.py | 4 - tests/test_antigravity.py | 4 + tests/test_circuit_breaker.py | 2 - tests/test_host_agy_daemon.py | 101 ++++--------------- tests/test_models_proxy.py | 7 -- tests/test_read_annotations_async.py | 26 ++--- 18 files changed, 120 insertions(+), 146 deletions(-) create mode 100644 tests/conftest.py diff --git a/.agents/branch_audit_plan.md b/.agents/branch_audit_plan.md index f1a9374c..28daae29 100644 --- a/.agents/branch_audit_plan.md +++ b/.agents/branch_audit_plan.md @@ -39,7 +39,7 @@ To ensure complete stability, we performed a systematic, automated three-dot dif ### The Regression * **File:** `router/main.py` -* **Issue:** `_load_aa_scores()` (which does a blocking synchronous JSON disk read of `aa_scores.json`) was called directly inside `compute_free_model_score()`. When resolving or syncing OpenRouter free models, this synchronous file read was executed sequentially in a loop, blocking the main event loop. +* **Issue:** `_load_aa_scores()` (which does a blocking synchronous JSON disk read of `aa_scores.json`) was called directly at free-model/roster call sites, specifically `sync_adaptive_router_roster()` and `get_best_free_model()`, rather than inside `compute_free_model_score()`. The fix was to move `_load_aa_scores()` behind `await asyncio.to_thread(...)` and remove the direct synchronous call from the scoring path. * **Merged Fix Lost:** The merged PR #98 had offloaded this operation to a background thread pool using `await asyncio.to_thread(_load_aa_scores)` outside the loops in `sync_adaptive_router_roster` and `get_best_free_model`, but this change was completely discarded during automated conflict resolution. ### The Resolution diff --git a/.agents/jules_regression_analysis.md b/.agents/jules_regression_analysis.md index 37b17229..ece7d2fd 100644 --- a/.agents/jules_regression_analysis.md +++ b/.agents/jules_regression_analysis.md @@ -32,7 +32,7 @@ git fetch origin git rebase origin/master ``` * **Why it works**: Rebase replays each feature branch commit one-by-one on top of the reorganized `master` commit. -* **Rename Tracking**: Git's rename tracking algorithm handles moves seamlessly during a rebase. If a file was renamed from `test_a2_verify.py` to `tests/test_a2_verify.py` on `master`, Git will automatically apply the feature branch's modifications to `tests/test_a2_verify.py` instead of leaving them at the root. +* **Verify Relocated Files**: While Git's rename detection can assist with directory/file moves during rebase, it is not foolproof. The agent must manually verify relocated files (such as renamed test paths) after rebasing to ensure all changes landed in their correct new locations rather than being lost or orphaned. ### Option B: Merge with explicit Merge Drivers If rebasing is not used, the agent must inspect renames before committing: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fc98319..0b7d3836 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: run: pip install httpx==0.28.1 pytest pytest-asyncio anyio pyyaml fastapi "pydantic>=2.0,<3.0" uvicorn python-multipart asyncpg langfuse redis aiofiles - name: Run Unit Tests - run: CONFIG_PATH=router/config.yaml PYTHONPATH=.:router pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py + run: CONFIG_PATH=router/config.yaml PYTHONPATH=.:router pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py - name: Run Breaker Verification run: PYTHONPATH=. python3 scripts/verification/verify_breaker.py diff --git a/pod.yaml b/pod.yaml index b8ed35b8..1f8ed291 100644 --- a/pod.yaml +++ b/pod.yaml @@ -187,7 +187,7 @@ spec: - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD - value: clickhouse + value: CLICKHOUSE_PASSWORD_PLACEHOLDER image: docker.io/clickhouse/clickhouse-server:26.6.1.1193 livenessProbe: exec: @@ -196,7 +196,7 @@ spec: - --user - clickhouse - --password - - clickhouse + - CLICKHOUSE_PASSWORD_PLACEHOLDER - --query - SELECT 1 initialDelaySeconds: 20 @@ -222,7 +222,7 @@ spec: - --port - '6380' - --requirepass - - langfuse-redis-2026 + - REDIS_AUTH_PLACEHOLDER - --maxmemory-policy - noeviction - --loglevel @@ -242,7 +242,7 @@ spec: - -p - "6380" - -a - - langfuse-redis-2026 + - REDIS_AUTH_PLACEHOLDER - ping initialDelaySeconds: 2 periodSeconds: 5 @@ -287,7 +287,7 @@ spec: - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD - value: clickhouse + value: CLICKHOUSE_PASSWORD_PLACEHOLDER - name: CLICKHOUSE_CLUSTER_ENABLED value: 'false' - name: REDIS_HOST @@ -295,7 +295,7 @@ spec: - name: REDIS_PORT value: '6380' - name: REDIS_AUTH - value: langfuse-redis-2026 + value: REDIS_AUTH_PLACEHOLDER - name: LANGFUSE_INIT_ORG_ID value: org-local-dev-id - name: LANGFUSE_INIT_ORG_NAME @@ -343,7 +343,7 @@ spec: - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD - value: clickhouse + value: CLICKHOUSE_PASSWORD_PLACEHOLDER - name: CLICKHOUSE_CLUSTER_ENABLED value: 'false' - name: REDIS_HOST @@ -351,7 +351,7 @@ spec: - name: REDIS_PORT value: '6380' - name: REDIS_AUTH - value: langfuse-redis-2026 + value: REDIS_AUTH_PLACEHOLDER - name: HOSTNAME value: 0.0.0.0 - name: PORT diff --git a/router/agy_proxy.py b/router/agy_proxy.py index 4e911c42..baf0ac91 100644 --- a/router/agy_proxy.py +++ b/router/agy_proxy.py @@ -164,6 +164,7 @@ async def _is_quota_exhausted(returncode: int, stdout: str, stderr: str) -> bool for line in content.splitlines()[-5:]: if "RESOURCE_EXHAUSTED" in line or "code 429" in line: return True + return False except Exception: pass # Empty stdout+stderr with rc=0 strongly suggests quota exhaustion diff --git a/router/memory_mcp.py b/router/memory_mcp.py index 326ad66a..ea946f9a 100755 --- a/router/memory_mcp.py +++ b/router/memory_mcp.py @@ -88,7 +88,14 @@ def _memory_value(data: str, tags: list | None) -> str: def _parse_memory_value(raw: str) -> dict: """Decode stored value back into {data, tags}.""" try: - return json.loads(raw) + val = json.loads(raw) + if not isinstance(val, dict): + return {"data": val, "tags": []} + if "data" not in val: + val["data"] = str(val) + if "tags" not in val or not isinstance(val["tags"], list): + val["tags"] = [] + return val except (json.JSONDecodeError, TypeError): return {"data": raw, "tags": []} @@ -230,7 +237,8 @@ async def handle_remove_memory_category(args: dict) -> str: async with httpx.AsyncClient(timeout=30.0) as client: for entry in to_delete: key = entry["key"] - await client.delete(f"{API_URL}/{key}", timeout=5.0) + quoted_key = urllib.parse.quote(key, safe="") + await client.delete(f"{API_URL}/{quoted_key}", timeout=5.0) scope_label = "global" if is_global else "local" cat_label = f"category '{category}'" if category != "*" else "all categories" @@ -269,7 +277,8 @@ async def handle_remove_specific_memory(args: dict) -> str: ) async with httpx.AsyncClient(timeout=10.0) as client: - r = await client.delete(f"{API_URL}/{target['key']}", timeout=5.0) + quoted_key = urllib.parse.quote(target['key'], safe="") + r = await client.delete(f"{API_URL}/{quoted_key}", timeout=5.0) if r.status_code == 200: return f"Removed memory in category '{category}' ({target['data'][:60]}...)." else: diff --git a/router/tests/test_get_gemini_oauth_status.py b/router/tests/test_get_gemini_oauth_status.py index 89523237..684a013f 100644 --- a/router/tests/test_get_gemini_oauth_status.py +++ b/router/tests/test_get_gemini_oauth_status.py @@ -9,6 +9,8 @@ if router_path not in sys.path: sys.path.insert(0, router_path) +os.environ["CONFIG_PATH"] = os.path.join(router_path, "config.yaml") + import main @pytest.mark.asyncio diff --git a/router/tests/test_load_persisted_stats.py b/router/tests/test_load_persisted_stats.py index ea0f9ad2..5e8e54d2 100644 --- a/router/tests/test_load_persisted_stats.py +++ b/router/tests/test_load_persisted_stats.py @@ -1,12 +1,19 @@ import os -os.environ.setdefault("CONFIG_PATH", "/config/router_dir/config.yaml") +import sys + +# Ensure router directory is in sys.path +router_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if router_path not in sys.path: + sys.path.insert(0, router_path) + +os.environ["CONFIG_PATH"] = os.path.join(router_path, "config.yaml") import json import pytest from unittest.mock import patch, mock_open -import router.main -from router.main import load_persisted_stats +import main +from main import load_persisted_stats @pytest.fixture def mock_stats(): @@ -16,13 +23,13 @@ def mock_stats(): "nested_dict": {"a": 1, "b": 2}, "existing_key": "value" } - with patch.dict(router.main.stats, clean_stats, clear=True): - yield router.main.stats + with patch.dict(main.stats, clean_stats, clear=True): + yield main.stats def test_load_persisted_stats_file_not_exists(mock_stats): - with patch("router.main.os.path.exists", return_value=False) as mock_exists: + with patch("main.os.path.exists", return_value=False) as mock_exists: load_persisted_stats() - mock_exists.assert_called_once_with(router.main.STATS_JSON_PATH) + mock_exists.assert_called_once_with(main.STATS_JSON_PATH) # Stats should remain unchanged assert mock_stats["total_requests"] == 0 @@ -34,9 +41,9 @@ def test_load_persisted_stats_success(mock_stats): } mock_json = json.dumps(mock_data) - with patch("router.main.os.path.exists", return_value=True): - with patch("router.main.open", mock_open(read_data=mock_json)): - with patch("router.main.logger.info") as mock_logger: + with patch("main.os.path.exists", return_value=True): + with patch("main.open", mock_open(read_data=mock_json)): + with patch("main.logger.info") as mock_logger: load_persisted_stats() # Assert simple value updated via else block @@ -51,9 +58,9 @@ def test_load_persisted_stats_success(mock_stats): mock_logger.assert_called_once_with("✓ Successfully loaded persisted gateway statistics from disk.") def test_load_persisted_stats_exception(mock_stats): - with patch("router.main.os.path.exists", return_value=True): - with patch("router.main.open", side_effect=Exception("Mock read error")): - with patch("router.main.logger.error") as mock_logger: + with patch("main.os.path.exists", return_value=True): + with patch("main.open", side_effect=Exception("Mock read error")): + with patch("main.logger.error") as mock_logger: load_persisted_stats() # Stats should remain unchanged diff --git a/router/tests/test_memory_mcp.py b/router/tests/test_memory_mcp.py index 236853eb..c5d52d7f 100644 --- a/router/tests/test_memory_mcp.py +++ b/router/tests/test_memory_mcp.py @@ -324,7 +324,7 @@ def test_parse_memory_value_type_error_fallback(): def test_parse_memory_value_non_dict_json(): raw_data = '"just a string"' result = _parse_memory_value(raw_data) - assert result == "just a string" + assert result == {"data": "just a string", "tags": []} def test_make_key_and_parse_key_round_trip(): diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py index 8000fa5e..e6512b21 100644 --- a/scripts/benchmark_classifier.py +++ b/scripts/benchmark_classifier.py @@ -57,12 +57,16 @@ def classify(prompt): confusion = defaultdict(Counter) # confusion[expected][predicted] def process_item(item): - prompt = item["prompt"] - expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") - try: + if not isinstance(item, dict): + raise TypeError("Item is not a dictionary") + prompt = item["prompt"] + expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") predicted = classify(prompt) except Exception as e: + expected = "" + if isinstance(item, dict): + expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") predicted = f"ERROR: {str(e)[:50]}" return expected, predicted diff --git a/start-stack.sh b/start-stack.sh index 4ad2fbcd..0b9b22ba 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -18,7 +18,6 @@ show_help() { echo " ./start-stack.sh --replace → Stop, clean up zombie ports, and recreate/redeploy pod" echo " ./start-stack.sh --full-rebuild → Rebuild router image and recreate/redeploy pod" echo " ./start-stack.sh --help | -h → Show this help message and exit" - exit 0 } FULL_REBUILD=false @@ -29,6 +28,7 @@ elif [ "${1:-}" = "--replace" ]; then REPLACE_MODE=true elif [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then show_help + exit 0 elif [ -n "${1:-}" ]; then echo "❌ Error: Unknown argument '${1}'" show_help @@ -230,9 +230,14 @@ fi if [ -z "$OLLAMA_API_KEY" ]; then if [ -t 0 ]; then echo "🔑 OLLAMA_API_KEY not found." - echo -n "Please enter your Ollama API Key (input will be hidden): " - read -rs OLLAMA_API_KEY - echo "" + while [ -z "$OLLAMA_API_KEY" ]; do + echo -n "Please enter your Ollama API Key (input will be hidden): " + read -rs OLLAMA_API_KEY + echo "" + if [ -z "$OLLAMA_API_KEY" ]; then + echo "❌ Error: API key cannot be empty. Please try again." + fi + done echo "OLLAMA_API_KEY=\"$OLLAMA_API_KEY\"" >> "$ENV_FILE" chmod 600 "$ENV_FILE" echo "✓ Ollama API key saved securely to $ENV_FILE" @@ -456,7 +461,7 @@ if podman pod exists agent-router-pod 2>/dev/null; then fi render_pod_yaml() { - export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY CLASSIFIER_INPUT_MAX_CHARS + export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY CLASSIFIER_INPUT_MAX_CHARS REDIS_AUTH CLICKHOUSE_PASSWORD python3 - "$WORKDIR/pod.yaml" <<'PY' import os, sys, urllib.parse uid = os.getuid() @@ -477,7 +482,9 @@ placeholders = [ "LANGFUSE_SECRET_KEY_PLACEHOLDER", "MINIO_USER_PLACEHOLDER", "MINIO_PASSWORD_PLACEHOLDER", - "LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER" + "LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", + "REDIS_AUTH_PLACEHOLDER", + "CLICKHOUSE_PASSWORD_PLACEHOLDER" ] for ph in placeholders: if ph not in text: @@ -500,6 +507,8 @@ text = text.replace("LANGFUSE_SECRET_KEY_PLACEHOLDER", os.environ["LANGFUSE_SECR text = text.replace("MINIO_USER_PLACEHOLDER", os.environ["MINIO_ROOT_USER"]) text = text.replace("MINIO_PASSWORD_PLACEHOLDER", os.environ["MINIO_ROOT_PASSWORD"]) text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", os.environ["LANGFUSE_INIT_USER_PASSWORD"]) +text = text.replace("REDIS_AUTH_PLACEHOLDER", os.environ["REDIS_AUTH"]) +text = text.replace("CLICKHOUSE_PASSWORD_PLACEHOLDER", os.environ["CLICKHOUSE_PASSWORD"]) sys.stdout.write(text) PY } diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..ae35288b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +import sys +import os +from pathlib import Path + +# Dynamic project root discovery +root = Path(__file__).resolve() +while root.parent != root and not (root / ".git").exists(): + root = root.parent + +# Insert paths to sys.path +for path in [str(root), str(root / "router"), str(root / "scripts")]: + if path not in sys.path: + sys.path.insert(0, path) + +# Set common environment variables +os.environ.setdefault("CONFIG_PATH", str(root / "router" / "config.yaml")) diff --git a/tests/test_a2_verify.py b/tests/test_a2_verify.py index d2f0be9a..95c6be31 100644 --- a/tests/test_a2_verify.py +++ b/tests/test_a2_verify.py @@ -1,9 +1,5 @@ #!/usr/bin/env python3 """Verify circuit breaker integration into agy_proxy.py""" -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'router')) - from circuit_breaker import get_breaker from agy_proxy import try_agy_proxy import asyncio, time diff --git a/tests/test_antigravity.py b/tests/test_antigravity.py index 8e0bc4b6..6b50d066 100644 --- a/tests/test_antigravity.py +++ b/tests/test_antigravity.py @@ -4,6 +4,10 @@ import time def test_antigravity_connection(): + if os.environ.get("GITHUB_ACTIONS") == "true": + import pytest + pytest.skip("Skipping antigravity connection test in CI.") + creds_path = os.path.expanduser("~/.gemini/oauth_creds.json") if not os.path.exists(creds_path): print(f"Error: {creds_path} not found.") diff --git a/tests/test_circuit_breaker.py b/tests/test_circuit_breaker.py index e4570c2a..695c0006 100644 --- a/tests/test_circuit_breaker.py +++ b/tests/test_circuit_breaker.py @@ -18,8 +18,6 @@ import asyncio import pytest from unittest.mock import patch, AsyncMock -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from router.circuit_breaker import get_breaker, TIER_COOLDOWNS, MAX_TIER diff --git a/tests/test_host_agy_daemon.py b/tests/test_host_agy_daemon.py index 61f84e86..86600be9 100644 --- a/tests/test_host_agy_daemon.py +++ b/tests/test_host_agy_daemon.py @@ -9,18 +9,15 @@ import pytest -import sys -from pathlib import Path - -# Dynamic project root discovery -root = Path(__file__).resolve() -while root.parent != root and not (root / ".git").exists(): - root = root.parent -sys.path.insert(0, str(root)) -sys.path.insert(0, str(root / "scripts")) - import host_agy_daemon +def make_run_request(daemon_server, payload): + return urllib.request.Request( + f"{daemon_server}/run", + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + def find_free_port(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('127.0.0.1', 0)) @@ -79,11 +76,7 @@ def test_daemon_post_404(daemon_server): assert exc.value.code == 404 def test_daemon_post_stream_false(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": False, "conversation_id": "conv_abc", "model_override": "gpt-4"}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False, "conversation_id": "conv_abc", "model_override": "gpt-4"}) async def mock_exec(*args, **kwargs): assert args == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_abc", "--print", "test prompt") @@ -113,11 +106,7 @@ async def mock_exec(*args, **kwargs): assert data["conversation_id"] == "last_conv_456" def test_daemon_post_stream_false_timeout(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": False, "timeout": 0.1}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False, "timeout": 0.1}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -139,11 +128,7 @@ async def slow_wait(): assert data["stderr"] == "TIMEOUT" def test_daemon_post_stream_true(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": True, "model_override": "test-model"}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True, "model_override": "test-model"}) async def mock_exec(*args, **kwargs): assert args == (host_agy_daemon.AGY_BINARY, "--print", "test prompt") @@ -179,11 +164,7 @@ def mock_read(fd, n): assert json.loads(lines[2]) == {"type": "status", "returncode": 0, "conversation_id": "last_conv_456"} def test_daemon_post_stream_true_exec_error(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": True}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True}) async def mock_exec(*args, **kwargs): raise Exception("exec failed") @@ -198,11 +179,7 @@ async def mock_exec(*args, **kwargs): assert json.loads(lines[0]) == {"type": "status", "returncode": -1, "stderr": "exec failed"} def test_daemon_post_stream_true_timeout(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": True, "timeout": 0.1}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True, "timeout": 0.1}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -261,11 +238,7 @@ def mock_server_close(self): assert close_called def test_daemon_post_stream_false_no_model_override(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False}) async def mock_exec(*args, **kwargs): assert "CASCADE_DEFAULT_MODEL_OVERRIDE" not in kwargs.get("env", {}) @@ -283,11 +256,7 @@ async def mock_exec(*args, **kwargs): assert data["returncode"] == 0 def test_daemon_post_stream_true_read_oserror(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": True}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -310,11 +279,7 @@ def mock_read(fd, n): assert json.loads(lines[0])["type"] == "status" def test_daemon_post_stream_true_timeout_kill_fail(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": True, "timeout": 0.1}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True, "timeout": 0.1}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -339,11 +304,7 @@ def mock_kill(): assert json.loads(lines[0]) == {"type": "status", "returncode": -1, "conversation_id": None} def test_daemon_post_stream_true_wait_exception(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": True}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -365,11 +326,7 @@ async def mock_wait(): assert json.loads(lines[0]) == {"type": "status", "returncode": -1, "conversation_id": None} def test_daemon_post_stream_false_timeout_kill_fail(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": False, "timeout": 0.1}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False, "timeout": 0.1}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -391,11 +348,7 @@ def mock_kill(): assert data["stderr"] == "TIMEOUT" def test_daemon_post_stream_false_wait_exception(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -413,11 +366,7 @@ async def mock_wait(): assert data["returncode"] == -1 def test_daemon_post_stream_false_file_read_error(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -440,11 +389,7 @@ async def mock_exec(*args, **kwargs): assert data["stderr"] == "" def test_daemon_post_stream_false_unlink_error(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": False}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False}) async def mock_exec(*args, **kwargs): mock_proc = AsyncMock() @@ -465,11 +410,7 @@ def mock_unlink(path): assert data["returncode"] == 0 def test_daemon_post_stream_true_with_conversation(daemon_server, monkeypatch): - req = urllib.request.Request( - f"{daemon_server}/run", - data=json.dumps({"prompt": "test prompt", "stream": True, "conversation_id": "conv_789"}).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) + req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True, "conversation_id": "conv_789"}) async def mock_exec(*args, **kwargs): assert args == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_789", "--print", "test prompt") diff --git a/tests/test_models_proxy.py b/tests/test_models_proxy.py index c325a39b..2ad68535 100644 --- a/tests/test_models_proxy.py +++ b/tests/test_models_proxy.py @@ -4,13 +4,6 @@ from fastapi import Response from fastapi.responses import JSONResponse -# Set CONFIG_PATH for import -os.environ["CONFIG_PATH"] = "router/config.yaml" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "router")) - from main import get_http_client, proxy_models, HTTP_MAX_CONNECTIONS, HTTP_MAX_KEEPALIVE_CONNECTIONS, HTTP_KEEPALIVE_EXPIRY def test_http_client_limits(): diff --git a/tests/test_read_annotations_async.py b/tests/test_read_annotations_async.py index b401bc8b..6a69cd26 100644 --- a/tests/test_read_annotations_async.py +++ b/tests/test_read_annotations_async.py @@ -12,6 +12,14 @@ import router.main from router.main import _read_annotations_async +def make_mock_aiofiles_open(content: str): + mock_file = AsyncMock() + mock_file.read.return_value = content + mock_context_manager = MagicMock() + mock_context_manager.__aenter__ = AsyncMock(return_value=mock_file) + mock_context_manager.__aexit__ = AsyncMock(return_value=False) + return MagicMock(return_value=mock_context_manager) + @pytest.fixture(autouse=True) def clear_cache(): router.main._annotations_cache.clear() @@ -23,14 +31,7 @@ async def test_read_annotations_async_initial_read(): fake_data = {"annotation1": "data1"} # Mock aiofiles.open - mock_file = AsyncMock() - mock_file.read.return_value = '{"annotation1": "data1"}' - - mock_context_manager = MagicMock() - mock_context_manager.__aenter__ = AsyncMock(return_value=mock_file) - mock_context_manager.__aexit__ = AsyncMock(return_value=False) - - mock_aiofiles_open = MagicMock(return_value=mock_context_manager) + mock_aiofiles_open = make_mock_aiofiles_open('{"annotation1": "data1"}') with patch("os.path.getmtime", return_value=100.0) as mock_getmtime, \ patch("aiofiles.open", mock_aiofiles_open) as mock_open: @@ -75,14 +76,7 @@ async def test_read_annotations_async_cache_invalidation(): # Pre-populate cache with old mtime router.main._annotations_cache[fake_path] = {"mtime": 100.0, "data": fake_data_old} - mock_file = AsyncMock() - mock_file.read.return_value = '{"annotation2": "data2"}' - - mock_context_manager = MagicMock() - mock_context_manager.__aenter__ = AsyncMock(return_value=mock_file) - mock_context_manager.__aexit__ = AsyncMock(return_value=False) - - mock_aiofiles_open = MagicMock(return_value=mock_context_manager) + mock_aiofiles_open = make_mock_aiofiles_open('{"annotation2": "data2"}') with patch("os.path.getmtime", return_value=200.0) as mock_getmtime, \ patch("aiofiles.open", mock_aiofiles_open) as mock_open: From 2eae001f0a3bda97ad49fe51768504762c0af086 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 15:48:36 +0200 Subject: [PATCH 19/22] Address and resolve PR #204 review feedback --- .agents/AGENTS.md | 2 +- .agents/branch_audit_plan.md | 2 +- README.md | 2 +- litellm/entrypoint.py | 4 +- router/main.py | 5 +- router/memory_mcp.py | 14 +++- router/tests/test_memory_mcp.py | 128 +++++++++++++++++++++++++++++--- scripts/benchmark_classifier.py | 10 ++- scripts/benchmark_tokens.py | 4 +- start-stack.sh | 52 +++++++++---- tests/conftest.py | 3 + 11 files changed, 189 insertions(+), 37 deletions(-) diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 260dee2c..16a25bb5 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -23,6 +23,6 @@ Use the `notebooklm` MCP tools to search or ask questions about this codebase an To prevent directory reorganization regressions, outdated file restorations, or security credential overrides during merge conflict resolution, all automated agents must strictly follow these rules: 1. **Rebase Over Merge**: Always fetch and rebase the topic/feature branch onto the latest `master` base branch (using `git rebase origin/master`) instead of performing `git merge`. -2. **Directory Rename Safety**: If Git reports conflicts related to moved directories or files, do not manually stage deletions of folders (`tests/`, `scripts/`) or re-create files at the root level. Direct all changes to the newly refactored paths. +2. **Directory Rename Safety**: If Git reports conflicts related to moved directories or files, do not manually stage deletions of tracked files from moved directories (e.g., under the old `tests/` or `scripts/` paths) or re-create files at the root level. Resolve conflicts by directing all changes and file operations to the newly refactored paths. 3. **Verify Security Credentials**: Never accept resolutions that overwrite configuration files (`pod.yaml`, `start-stack.sh`) with hardcoded default passwords. Ensure placeholder-based configurations are preserved. 4. **Enforce Test Suite Count**: Run the full unit test suite (`pytest`) after conflict resolution. Verify that the total number of passing tests is equal to or greater than before the resolution. diff --git a/.agents/branch_audit_plan.md b/.agents/branch_audit_plan.md index 28daae29..daa2e840 100644 --- a/.agents/branch_audit_plan.md +++ b/.agents/branch_audit_plan.md @@ -53,4 +53,4 @@ To ensure complete stability, we performed a systematic, automated three-dot dif ## Verification Status * **Test Suite:** Ran `PYTHONPATH=router:. pytest` -* **Result:** **176 tests passed** successfully (100% success rate). +* **Result:** **181 tests passed** successfully (100% success rate). diff --git a/README.md b/README.md index bce2a245..2d729763 100644 --- a/README.md +++ b/README.md @@ -793,7 +793,7 @@ For auto-routing modes, the Triage Router handles failures by silently falling b ## 9d. Live Stack Tier Testing & Verification The repository includes an automated integration script to test the 5-tier intent routing pipeline on the live gateway stack: -* **Location**: [test_reasoning_tiers.py](file:///home/gpav/Vrac/LAB/AI/LLM-Routing/scripts/verification/test_reasoning_tiers.py) +* **Location**: [test_reasoning_tiers.py](scripts/verification/test_reasoning_tiers.py) This script sends five sequential chat completion requests (from simple to advanced prompt complexities) to the gateway's `llm-routing-auto-free` auto-triage route, verifying that: 1. The local classifier correctly categorizes and labels the prompt intent. diff --git a/litellm/entrypoint.py b/litellm/entrypoint.py index df7a2ec8..192cbb1b 100644 --- a/litellm/entrypoint.py +++ b/litellm/entrypoint.py @@ -114,7 +114,9 @@ def _serialize_dt(dt): """Serialize datetime to ISO8601 with timezone (UTC if naive).""" if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat() + else: + dt = dt.astimezone(timezone.utc) + return dt.isoformat().replace("+00:00", "Z") serializer.register(original_datetime, _serialize_dt) serializer.register(RobustDatetime, _serialize_dt) print("🩹 Registered original_datetime + RobustDatetime with Prisma serializer") diff --git a/router/main.py b/router/main.py index c26a7e40..4074efdc 100644 --- a/router/main.py +++ b/router/main.py @@ -123,6 +123,9 @@ def _count_tokens_heuristic(text: str) -> float: return word_total + (non_ascii_count * 0.35) + (punc_count * 0.4) +METADATA_OVERHEAD = 50 + + def estimate_prompt_tokens(body: dict) -> int: """Estimate prompt tokens using a regex-based weighted heuristic for mixed content. """ @@ -142,7 +145,7 @@ def estimate_prompt_tokens(body: dict) -> int: # Include a flat estimate for system prompt / metadata overhead. # Use rounding to avoid truncation bias (e.g., 1.9 -> 1). - return max(1, round(total) + 50) + return max(1, round(total) + METADATA_OVERHEAD) async def sync_cooldowns_from_valkey() -> None: diff --git a/router/memory_mcp.py b/router/memory_mcp.py index ea946f9a..a438ad1d 100755 --- a/router/memory_mcp.py +++ b/router/memory_mcp.py @@ -93,6 +93,9 @@ def _parse_memory_value(raw: str) -> dict: return {"data": val, "tags": []} if "data" not in val: val["data"] = str(val) + else: + if val["data"] is not None: + val["data"] = str(val["data"]) if "tags" not in val or not isinstance(val["tags"], list): val["tags"] = [] return val @@ -234,15 +237,22 @@ async def handle_remove_memory_category(args: dict) -> str: scope_label = "global" if is_global else "local" return f"No memories found to remove in category '{category}' ({scope_label})." + deleted_count = 0 async with httpx.AsyncClient(timeout=30.0) as client: for entry in to_delete: key = entry["key"] quoted_key = urllib.parse.quote(key, safe="") - await client.delete(f"{API_URL}/{quoted_key}", timeout=5.0) + r = await client.delete(f"{API_URL}/{quoted_key}", timeout=5.0) + if r.status_code == 200: + deleted_count += 1 + else: + scope_label = "global" if is_global else "local" + cat_label = f"category '{category}'" if category != "*" else "all categories" + return f"Error removing memory (deleted {deleted_count} of {len(to_delete)} from {cat_label} ({scope_label})): {r.text}" scope_label = "global" if is_global else "local" cat_label = f"category '{category}'" if category != "*" else "all categories" - return f"Removed {len(to_delete)} memory(ies) from {cat_label} ({scope_label})." + return f"Removed {deleted_count} memory(ies) from {cat_label} ({scope_label})." async def handle_remove_specific_memory(args: dict) -> str: diff --git a/router/tests/test_memory_mcp.py b/router/tests/test_memory_mcp.py index c5d52d7f..ff680ad4 100644 --- a/router/tests/test_memory_mcp.py +++ b/router/tests/test_memory_mcp.py @@ -1,16 +1,8 @@ import json import re -import sys import time -from pathlib import Path - -# Dynamic project root discovery -root = Path(__file__).resolve() -while root.parent != root and not (root / ".git").exists(): - root = root.parent -sys.path.insert(0, str(root)) -sys.path.insert(0, str(root / "router")) - +import urllib.parse +from unittest.mock import AsyncMock, MagicMock, patch import pytest from memory_mcp import ( PREFIX, @@ -339,3 +331,119 @@ def test_make_key_and_parse_key_round_trip(): parsed = _parse_key(key) assert parsed["scope"] == "local" assert parsed["category"] == category + + +@pytest.mark.asyncio +async def test_handle_remove_memory_category_url_encoding(): + from memory_mcp import handle_remove_memory_category + + category = "test:cat/100%_done" + key = _make_key(category, is_global=False, data="some-value") + + mock_list_response = MagicMock() + mock_list_response.status_code = 200 + mock_list_response.json.return_value = { + "memories": [ + { + "key": key, + "value": _memory_value("some-value", ["tag1"]) + } + ] + } + + mock_delete_response = MagicMock() + mock_delete_response.status_code = 200 + + mock_client = AsyncMock() + mock_client.get.return_value = mock_list_response + mock_client.delete.return_value = mock_delete_response + + with patch("httpx.AsyncClient") as mock_client_class: + mock_client_class.return_value.__aenter__.return_value = mock_client + + result = await handle_remove_memory_category({"category": category, "is_global": False}) + + assert "Removed 1 memory" in result + + expected_quoted_key = urllib.parse.quote(key, safe="") + mock_client.delete.assert_called_once() + called_url = mock_client.delete.call_args[0][0] + assert called_url.endswith(expected_quoted_key) + + +@pytest.mark.asyncio +async def test_handle_remove_specific_memory_url_encoding(): + from memory_mcp import handle_remove_specific_memory + + category = "test:cat/100%_done" + key = _make_key(category, is_global=False, data="some-value") + + mock_list_response = MagicMock() + mock_list_response.status_code = 200 + mock_list_response.json.return_value = { + "memories": [ + { + "key": key, + "value": _memory_value("some-value", ["tag1"]) + } + ] + } + + mock_delete_response = MagicMock() + mock_delete_response.status_code = 200 + + mock_client = AsyncMock() + mock_client.get.return_value = mock_list_response + mock_client.delete.return_value = mock_delete_response + + with patch("httpx.AsyncClient") as mock_client_class: + mock_client_class.return_value.__aenter__.return_value = mock_client + + result = await handle_remove_specific_memory({ + "category": category, + "memory_content": "some-value", + "is_global": False + }) + + assert "Removed memory" in result + + expected_quoted_key = urllib.parse.quote(key, safe="") + mock_client.delete.assert_called_once() + called_url = mock_client.delete.call_args[0][0] + assert called_url.endswith(expected_quoted_key) + + +@pytest.mark.asyncio +async def test_handle_remove_memory_category_failure(): + from memory_mcp import handle_remove_memory_category + + key1 = _make_key("cat", is_global=False, data="val1") + key2 = _make_key("cat", is_global=False, data="val2") + + mock_list_response = MagicMock() + mock_list_response.status_code = 200 + mock_list_response.json.return_value = { + "memories": [ + {"key": key1, "value": _memory_value("val1", [])}, + {"key": key2, "value": _memory_value("val2", [])} + ] + } + + mock_response_200 = MagicMock() + mock_response_200.status_code = 200 + mock_response_500 = MagicMock() + mock_response_500.status_code = 500 + mock_response_500.text = "Internal Server Error" + + mock_client = AsyncMock() + mock_client.get.return_value = mock_list_response + mock_client.delete.side_effect = [mock_response_200, mock_response_500] + + with patch("httpx.AsyncClient") as mock_client_class: + mock_client_class.return_value.__aenter__.return_value = mock_client + + result = await handle_remove_memory_category({"category": "cat", "is_global": False}) + + assert "Error removing memory" in result + assert "deleted 1 of 2" in result + assert "Internal Server Error" in result diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py index e6512b21..4786a626 100644 --- a/scripts/benchmark_classifier.py +++ b/scripts/benchmark_classifier.py @@ -73,7 +73,7 @@ def process_item(item): results_list = [None] * total -with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor: +with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: sem = threading.Semaphore(5) def process_item_with_rate_limit(index_and_item): @@ -90,8 +90,14 @@ def process_item_with_rate_limit(index_and_item): for future in concurrent.futures.as_completed(futures): i, item, expected, predicted = future.result() + prompt_val = "" + if isinstance(item, dict): + prompt_val = str(item.get("prompt", ""))[:100] + else: + prompt_val = f"" + results_list[i] = { - "prompt": item["prompt"][:100], + "prompt": prompt_val, "expected": expected, "predicted": predicted, } diff --git a/scripts/benchmark_tokens.py b/scripts/benchmark_tokens.py index 1fe8da63..73be4033 100644 --- a/scripts/benchmark_tokens.py +++ b/scripts/benchmark_tokens.py @@ -9,7 +9,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "router")) -from router.main import estimate_prompt_tokens +from router.main import estimate_prompt_tokens, METADATA_OVERHEAD def verify_accuracy(): """Benchmarking utility to verify token estimation accuracy across content types.""" @@ -57,7 +57,7 @@ def calculate_factorial(n): all_passed = True for case in test_cases: body = {"messages": [{"content": case["content"]}]} - est = estimate_prompt_tokens(body) - 50 # Subtract metadata overhead + est = estimate_prompt_tokens(body) - METADATA_OVERHEAD # Subtract metadata overhead error = abs(est - case["actual_tokens"]) / case["actual_tokens"] print(f"{case['name']:<25} | {case['actual_tokens']:<7} | {est:<9} | {error:.1%}") # Acceptance criteria: within ±25% for these rough heuristics diff --git a/start-stack.sh b/start-stack.sh index 0b9b22ba..7195ee19 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -20,6 +20,20 @@ show_help() { echo " ./start-stack.sh --help | -h → Show this help message and exit" } +escape_env_val() { + local val="$1" + val="${val//\\/\\\\}" + val="${val//\"/\\\"}" + echo "$val" +} + + +if [ $# -gt 1 ]; then + echo "❌ Error: Too many arguments supplied (expected at most 1, got $#)" + show_help + exit 1 +fi + FULL_REBUILD=false REPLACE_MODE=false if [ "${1:-}" = "--full-rebuild" ]; then @@ -67,7 +81,8 @@ if [ -z "$OPENROUTER_API_KEY" ]; then echo -n "Please enter your OpenRouter API Key (input will be hidden): " read -rs OPENROUTER_API_KEY echo "" - echo "OPENROUTER_API_KEY=\"$OPENROUTER_API_KEY\"" >> "$ENV_FILE" + local escaped_key=$(escape_env_val "$OPENROUTER_API_KEY") + echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE" chmod 600 "$ENV_FILE" echo "✓ API key saved securely to $ENV_FILE" else @@ -238,7 +253,8 @@ if [ -z "$OLLAMA_API_KEY" ]; then echo "❌ Error: API key cannot be empty. Please try again." fi done - echo "OLLAMA_API_KEY=\"$OLLAMA_API_KEY\"" >> "$ENV_FILE" + local escaped_key=$(escape_env_val "$OLLAMA_API_KEY") + echo "OLLAMA_API_KEY=\"$escaped_key\"" >> "$ENV_FILE" chmod 600 "$ENV_FILE" echo "✓ Ollama API key saved securely to $ENV_FILE" else @@ -463,10 +479,14 @@ fi render_pod_yaml() { export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY CLASSIFIER_INPUT_MAX_CHARS REDIS_AUTH CLICKHOUSE_PASSWORD python3 - "$WORKDIR/pod.yaml" <<'PY' -import os, sys, urllib.parse +import os, sys, urllib.parse, json uid = os.getuid() with open(sys.argv[1], "r", encoding="utf-8") as f: text = f.read() + +def yaml_scalar(val): + return json.dumps(val) + placeholders = [ "/home/gpav/Vrac/LAB/AI/LLM-Routing", "/home/gpav/", @@ -493,22 +513,22 @@ for ph in placeholders: text = text.replace("/home/gpav/Vrac/LAB/AI/LLM-Routing", os.environ["WORKDIR"]) text = text.replace("/home/gpav/", os.environ["HOME"] + "/") text = text.replace("/run/user/1000", f"/run/user/{uid}") -text = text.replace("LITELLM_MASTER_KEY_PLACEHOLDER", os.environ["LITELLM_MASTER_KEY"]) -text = text.replace("POSTGRES_PASSWORD_RAW_PLACEHOLDER", os.environ["POSTGRES_PASSWORD"]) +text = text.replace("LITELLM_MASTER_KEY_PLACEHOLDER", yaml_scalar(os.environ["LITELLM_MASTER_KEY"])) +text = text.replace("POSTGRES_PASSWORD_RAW_PLACEHOLDER", yaml_scalar(os.environ["POSTGRES_PASSWORD"])) # URL-encode the postgres password for DSN insertion encoded_password = urllib.parse.quote(os.environ['POSTGRES_PASSWORD'], safe="") text = text.replace("POSTGRES_PASSWORD_ENCODED_PLACEHOLDER", encoded_password) -text = text.replace("NEXTAUTH_SECRET_PLACEHOLDER", os.environ["NEXTAUTH_SECRET"]) -text = text.replace("SALT_PLACEHOLDER", os.environ["SALT"]) -text = text.replace("ENCRYPTION_KEY_PLACEHOLDER", os.environ["ENCRYPTION_KEY"]) -text = text.replace("OLLAMA_API_KEY_PLACEHOLDER", os.environ["OLLAMA_API_KEY"]) -text = text.replace("LANGFUSE_PUBLIC_KEY_PLACEHOLDER", os.environ["LANGFUSE_PUBLIC_KEY"]) -text = text.replace("LANGFUSE_SECRET_KEY_PLACEHOLDER", os.environ["LANGFUSE_SECRET_KEY"]) -text = text.replace("MINIO_USER_PLACEHOLDER", os.environ["MINIO_ROOT_USER"]) -text = text.replace("MINIO_PASSWORD_PLACEHOLDER", os.environ["MINIO_ROOT_PASSWORD"]) -text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", os.environ["LANGFUSE_INIT_USER_PASSWORD"]) -text = text.replace("REDIS_AUTH_PLACEHOLDER", os.environ["REDIS_AUTH"]) -text = text.replace("CLICKHOUSE_PASSWORD_PLACEHOLDER", os.environ["CLICKHOUSE_PASSWORD"]) +text = text.replace("NEXTAUTH_SECRET_PLACEHOLDER", yaml_scalar(os.environ["NEXTAUTH_SECRET"])) +text = text.replace("SALT_PLACEHOLDER", yaml_scalar(os.environ["SALT"])) +text = text.replace("ENCRYPTION_KEY_PLACEHOLDER", yaml_scalar(os.environ["ENCRYPTION_KEY"])) +text = text.replace("OLLAMA_API_KEY_PLACEHOLDER", yaml_scalar(os.environ["OLLAMA_API_KEY"])) +text = text.replace("LANGFUSE_PUBLIC_KEY_PLACEHOLDER", yaml_scalar(os.environ["LANGFUSE_PUBLIC_KEY"])) +text = text.replace("LANGFUSE_SECRET_KEY_PLACEHOLDER", yaml_scalar(os.environ["LANGFUSE_SECRET_KEY"])) +text = text.replace("MINIO_USER_PLACEHOLDER", yaml_scalar(os.environ["MINIO_ROOT_USER"])) +text = text.replace("MINIO_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ["MINIO_ROOT_PASSWORD"])) +text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ["LANGFUSE_INIT_USER_PASSWORD"])) +text = text.replace("REDIS_AUTH_PLACEHOLDER", yaml_scalar(os.environ["REDIS_AUTH"])) +text = text.replace("CLICKHOUSE_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ["CLICKHOUSE_PASSWORD"])) sys.stdout.write(text) PY } diff --git a/tests/conftest.py b/tests/conftest.py index ae35288b..a4c78758 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,9 @@ while root.parent != root and not (root / ".git").exists(): root = root.parent +if not (root / ".git").exists(): + raise RuntimeError("Could not find project root: .git directory not found in parent paths.") + # Insert paths to sys.path for path in [str(root), str(root / "router"), str(root / "scripts")]: if path not in sys.path: From ee7584d3db28c0cbdc9b29acb85a362c716ab1fd Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 17:26:36 +0200 Subject: [PATCH 20/22] Fix bash syntax error: remove local outside function --- start-stack.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/start-stack.sh b/start-stack.sh index 7195ee19..3ea320e9 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -81,7 +81,7 @@ if [ -z "$OPENROUTER_API_KEY" ]; then echo -n "Please enter your OpenRouter API Key (input will be hidden): " read -rs OPENROUTER_API_KEY echo "" - local escaped_key=$(escape_env_val "$OPENROUTER_API_KEY") + escaped_key=$(escape_env_val "$OPENROUTER_API_KEY") echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE" chmod 600 "$ENV_FILE" echo "✓ API key saved securely to $ENV_FILE" @@ -253,7 +253,7 @@ if [ -z "$OLLAMA_API_KEY" ]; then echo "❌ Error: API key cannot be empty. Please try again." fi done - local escaped_key=$(escape_env_val "$OLLAMA_API_KEY") + escaped_key=$(escape_env_val "$OLLAMA_API_KEY") echo "OLLAMA_API_KEY=\"$escaped_key\"" >> "$ENV_FILE" chmod 600 "$ENV_FILE" echo "✓ Ollama API key saved securely to $ENV_FILE" From 5f86090e35a29b22beb572c1edfcea2db3ad57a6 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Thu, 2 Jul 2026 22:44:40 +0200 Subject: [PATCH 21/22] chore: address unaddressed PR 205 review findings --- .github/workflows/test.yml | 2 +- litellm/entrypoint.py | 6 ++- pytest.ini | 2 +- router/memory_mcp.py | 29 +++++++++++---- router/tests/conftest.py | 9 +++++ router/tests/test_detect_active_tool.py | 10 ----- router/tests/test_estimate_prompt_tokens.py | 10 ----- router/tests/test_get_gemini_oauth_status.py | 9 ----- router/tests/test_get_goose_sessions.py | 5 --- router/tests/test_load_persisted_stats.py | 12 +----- scripts/benchmark_classifier.py | 13 +++++-- scripts/verification/test_reasoning_tiers.py | 39 ++++++++++++++++---- start-stack.sh | 2 + 13 files changed, 81 insertions(+), 67 deletions(-) create mode 100644 router/tests/conftest.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b7d3836..03beb7ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: run: pip install httpx==0.28.1 pytest pytest-asyncio anyio pyyaml fastapi "pydantic>=2.0,<3.0" uvicorn python-multipart asyncpg langfuse redis aiofiles - name: Run Unit Tests - run: CONFIG_PATH=router/config.yaml PYTHONPATH=.:router pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py + run: CONFIG_PATH=router/config.yaml PYTHONPATH=.:router pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py --ignore=scripts/verification - name: Run Breaker Verification run: PYTHONPATH=. python3 scripts/verification/verify_breaker.py diff --git a/litellm/entrypoint.py b/litellm/entrypoint.py index 192cbb1b..65520978 100644 --- a/litellm/entrypoint.py +++ b/litellm/entrypoint.py @@ -106,13 +106,15 @@ def strptime(cls, date_str: str, fmt: str) -> original_datetime: # instances, which no longer match. We must register both classes. try: from prisma.builder import serializer -except ImportError: +except ImportError as e: + if e.name is not None and e.name not in ("prisma", "prisma.builder", "serializer"): + raise serializer = None if serializer is not None: def _serialize_dt(dt): """Serialize datetime to ISO8601 with timezone (UTC if naive).""" - if dt.tzinfo is None: + if dt.utcoffset() is None: dt = dt.replace(tzinfo=timezone.utc) else: dt = dt.astimezone(timezone.utc) diff --git a/pytest.ini b/pytest.ini index 7edbc609..25602b37 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] addopts = --import-mode=importlib -norecursedirs = clickhouse-data postgres-data langfuse-data redis-lf-data valkey-data minio-data .git .github +norecursedirs = clickhouse-data postgres-data langfuse-data redis-lf-data valkey-data minio-data .git .github scripts asyncio_mode = auto asyncio_default_fixture_loop_scope = function diff --git a/router/memory_mcp.py b/router/memory_mcp.py index a438ad1d..a1d33c8d 100755 --- a/router/memory_mcp.py +++ b/router/memory_mcp.py @@ -18,6 +18,7 @@ import hashlib import httpx import urllib.parse +import asyncio API_URL = "http://127.0.0.1:5000/v1/memory" PROTOCOL_VERSION = "2024-11-05" @@ -238,17 +239,31 @@ async def handle_remove_memory_category(args: dict) -> str: return f"No memories found to remove in category '{category}' ({scope_label})." deleted_count = 0 - async with httpx.AsyncClient(timeout=30.0) as client: - for entry in to_delete: + + async def delete_item(client, entry, sem): + nonlocal deleted_count + async with sem: key = entry["key"] quoted_key = urllib.parse.quote(key, safe="") - r = await client.delete(f"{API_URL}/{quoted_key}", timeout=5.0) - if r.status_code == 200: - deleted_count += 1 - else: + try: + r = await client.delete(f"{API_URL}/{quoted_key}", timeout=5.0) + if r.status_code == 200: + deleted_count += 1 + return None + else: + return r.text + except Exception as e: + return str(e) + + sem = asyncio.Semaphore(10) + async with httpx.AsyncClient(timeout=30.0) as client: + tasks = [delete_item(client, entry, sem) for entry in to_delete] + results = await asyncio.gather(*tasks) + for res in results: + if res is not None: scope_label = "global" if is_global else "local" cat_label = f"category '{category}'" if category != "*" else "all categories" - return f"Error removing memory (deleted {deleted_count} of {len(to_delete)} from {cat_label} ({scope_label})): {r.text}" + return f"Error removing memory (deleted {deleted_count} of {len(to_delete)} from {cat_label} ({scope_label})): {res}" scope_label = "global" if is_global else "local" cat_label = f"category '{category}'" if category != "*" else "all categories" diff --git a/router/tests/conftest.py b/router/tests/conftest.py new file mode 100644 index 00000000..bde2a6fc --- /dev/null +++ b/router/tests/conftest.py @@ -0,0 +1,9 @@ +import os +import sys +from pathlib import Path + +# Set CONFIG_PATH for import +os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml") + +# Add the parent directory to the path so we can import from router +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) diff --git a/router/tests/test_detect_active_tool.py b/router/tests/test_detect_active_tool.py index c5b359d1..0bebb391 100644 --- a/router/tests/test_detect_active_tool.py +++ b/router/tests/test_detect_active_tool.py @@ -1,14 +1,4 @@ import pytest -import os -import sys -from pathlib import Path - -# Set CONFIG_PATH for import -os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml") - -# Add the parent directory to the path so we can import from router -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - from main import detect_active_tool def test_detect_active_tool_empty(): diff --git a/router/tests/test_estimate_prompt_tokens.py b/router/tests/test_estimate_prompt_tokens.py index ae74a9e5..32da0463 100644 --- a/router/tests/test_estimate_prompt_tokens.py +++ b/router/tests/test_estimate_prompt_tokens.py @@ -1,14 +1,4 @@ import pytest -import sys -import os -from pathlib import Path - -# Set CONFIG_PATH for import -os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml") - -# Add the parent directory to the path so we can import from router -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - from main import estimate_prompt_tokens def test_estimate_prompt_tokens_empty(): diff --git a/router/tests/test_get_gemini_oauth_status.py b/router/tests/test_get_gemini_oauth_status.py index 684a013f..0fce2243 100644 --- a/router/tests/test_get_gemini_oauth_status.py +++ b/router/tests/test_get_gemini_oauth_status.py @@ -1,16 +1,7 @@ import pytest -import os -import sys import json from unittest.mock import patch, mock_open -# Ensure router directory is in sys.path -router_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -if router_path not in sys.path: - sys.path.insert(0, router_path) - -os.environ["CONFIG_PATH"] = os.path.join(router_path, "config.yaml") - import main @pytest.mark.asyncio diff --git a/router/tests/test_get_goose_sessions.py b/router/tests/test_get_goose_sessions.py index 52640fef..45de1a05 100644 --- a/router/tests/test_get_goose_sessions.py +++ b/router/tests/test_get_goose_sessions.py @@ -1,12 +1,7 @@ import pytest import sys -import os -from pathlib import Path from unittest.mock import patch, MagicMock -os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml") -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - from main import get_goose_sessions def test_get_goose_sessions_no_db(): diff --git a/router/tests/test_load_persisted_stats.py b/router/tests/test_load_persisted_stats.py index 5e8e54d2..67458d50 100644 --- a/router/tests/test_load_persisted_stats.py +++ b/router/tests/test_load_persisted_stats.py @@ -1,13 +1,3 @@ -import os -import sys - -# Ensure router directory is in sys.path -router_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -if router_path not in sys.path: - sys.path.insert(0, router_path) - -os.environ["CONFIG_PATH"] = os.path.join(router_path, "config.yaml") - import json import pytest from unittest.mock import patch, mock_open @@ -41,7 +31,7 @@ def test_load_persisted_stats_success(mock_stats): } mock_json = json.dumps(mock_data) - with patch("main.os.path.exists", return_value=True): + with patch("main.os.path.exists", side_effect=lambda p: p == main.STATS_JSON_PATH): with patch("main.open", mock_open(read_data=mock_json)): with patch("main.logger.info") as mock_logger: load_persisted_stats() diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py index 4786a626..70f68442 100644 --- a/scripts/benchmark_classifier.py +++ b/scripts/benchmark_classifier.py @@ -1,6 +1,6 @@ """Benchmark gemma4-26a4b-routing classifier against labeled dataset.""" import os -import json, urllib.request, time, sys +import json, urllib.request, urllib.error, time, sys import concurrent.futures import threading from collections import defaultdict, Counter @@ -63,7 +63,7 @@ def process_item(item): prompt = item["prompt"] expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") predicted = classify(prompt) - except Exception as e: + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, TypeError) as e: expected = "" if isinstance(item, dict): expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") @@ -75,12 +75,17 @@ def process_item(item): with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: sem = threading.Semaphore(5) + rate_lock = threading.Lock() + next_start_time = [time.monotonic()] def process_item_with_rate_limit(index_and_item): i, item = index_and_item with sem: - # Add a small delay between acquisitions to stagger server load - time.sleep(0.05) + with rate_lock: + now = time.monotonic() + if now < next_start_time[0]: + time.sleep(next_start_time[0] - now) + next_start_time[0] = time.monotonic() + 0.05 expected, predicted = process_item(item) return i, item, expected, predicted diff --git a/scripts/verification/test_reasoning_tiers.py b/scripts/verification/test_reasoning_tiers.py index 6f702300..1bc0360b 100755 --- a/scripts/verification/test_reasoning_tiers.py +++ b/scripts/verification/test_reasoning_tiers.py @@ -10,39 +10,47 @@ import httpx import json import time +import os +import sys -gateway_url = "http://127.0.0.1:5000/v1/chat/completions" +gateway_url = os.environ.get("GATEWAY_URL", "http://127.0.0.1:5000/v1/chat/completions") headers = { - "Authorization": "Bearer gateway-pass", + "Authorization": f"Bearer {os.environ.get('GATEWAY_TOKEN', 'gateway-pass')}", "Content-Type": "application/json" } prompts = [ { "name": "Simple (1/5)", - "prompt": "Write a one-line hello world in Python." + "prompt": "Write a one-line hello world in Python.", + "expected_model": "agent-simple-core" }, { "name": "Medium (2/5)", - "prompt": "Refactor this Python function to add a docstring and use a type hint: def sum(a, b): return a + b" + "prompt": "Refactor this Python function to add a docstring and use a type hint: def sum(a, b): return a + b", + "expected_model": "agent-medium-core" }, { "name": "Complex (3/5)", - "prompt": "Write a multi-file Python script implementing a complete ETL data pipeline that parses a complex nested JSON file of users and logs, validates the data with Pydantic, writes it to a PostgreSQL database using asyncpg, handles connection pool retries with backoff, and includes unit tests with pytest mocks." + "prompt": "Write a multi-file Python script implementing a complete ETL data pipeline that parses a complex nested JSON file of users and logs, validates the data with Pydantic, writes it to a PostgreSQL database using asyncpg, handles connection pool retries with backoff, and includes unit tests with pytest mocks.", + "expected_model": "agent-complex-core" }, { "name": "Reasoning (4/5)", - "prompt": "Compare the design trade-offs of using Valkey/Redis Sentinel versus Valkey/Redis Cluster for high availability in a high-throughput, low-latency microservice architecture. Analyze failure detection mechanisms, failover times, read/write scaling, client complexity, and network partition (split-brain) scenarios." + "prompt": "Compare the design trade-offs of using Valkey/Redis Sentinel versus Valkey/Redis Cluster for high availability in a high-throughput, low-latency microservice architecture. Analyze failure detection mechanisms, failover times, read/write scaling, client complexity, and network partition (split-brain) scenarios.", + "expected_model": "agent-reasoning-core" }, { "name": "Advanced (5/5)", - "prompt": "Design a highly secure, zero-trust microservice topology for a federated agent-routing system deployed across multiple Kubernetes clusters in different cloud regions. Incorporate mutual TLS via Istio service mesh, automated certificate rotation with cert-manager, OIDC/OAuth2 authentication via an external identity provider, centralized audit logging/distributed tracing using Langfuse and Clickhouse, regional failover routing based on latency, and DDoS protection." + "prompt": "Design a highly secure, zero-trust microservice topology for a federated agent-routing system deployed across multiple Kubernetes clusters in different cloud regions. Incorporate mutual TLS via Istio service mesh, automated certificate rotation with cert-manager, OIDC/OAuth2 authentication via an external identity provider, centralized audit logging/distributed tracing using Langfuse and Clickhouse, regional failover routing based on latency, and DDoS protection.", + "expected_model": "agent-advanced-core" } ] def run_tests(): print("🚀 Starting 5-tier reasoning test queries...") print("=" * 60) + failed = False for p in prompts: print(f"\n📂 Sending {p['name']} Prompt:") print(f" Prompt: {p['prompt'][:100]}...") @@ -74,14 +82,31 @@ def run_tests(): content_clean = "" print(f" Routed Model: {model_used}") print(f" Response Preview: {content_clean[:120]}...") + + expected = p["expected_model"] + if model_used != expected: + print(f" ❌ Mismatch! Expected: {expected}, Got: {model_used}") + failed = True + else: + print(f" ✅ Routed correctly to {expected}") except Exception as parse_inner: print(f" Failed to parse response choices: {parse_inner}") print(f" Raw Data: {data}") + failed = True print(f" Latency: {elapsed:.2f}s") else: print(f" Error: {r.text}") + failed = True except Exception as e: print(f" Request Exception: {e}") + failed = True + if failed: + print("\n❌ Verification Failed: Some tests misrouted or encountered errors.") + sys.exit(1) + else: + print("\n✅ Verification Successful: All tests routed to expected models.") + sys.exit(0) + if __name__ == "__main__": run_tests() diff --git a/start-stack.sh b/start-stack.sh index 3ea320e9..3b01be94 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -24,6 +24,8 @@ escape_env_val() { local val="$1" val="${val//\\/\\\\}" val="${val//\"/\\\"}" + val="${val//\$/\\\$}" + val="${val//\`/\\\`}" echo "$val" } From af6489c06e4e56fc2c09c242577e85bed5d6f083 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Fri, 3 Jul 2026 00:21:36 +0200 Subject: [PATCH 22/22] chore: address PR 206 review feedback and implement declarative PGDATA version-gating --- .agents/jules_regression_analysis.md | 6 ++--- README.md | 7 +++-- pod.yaml | 2 ++ router/memory_mcp.py | 2 +- scripts/benchmark_classifier.py | 2 +- start-stack.sh | 39 +++++++++++++++++++++++----- tests/test_host_agy_daemon.py | 24 ++++++++++++----- 7 files changed, 60 insertions(+), 22 deletions(-) diff --git a/.agents/jules_regression_analysis.md b/.agents/jules_regression_analysis.md index ece7d2fd..624477bf 100644 --- a/.agents/jules_regression_analysis.md +++ b/.agents/jules_regression_analysis.md @@ -55,7 +55,7 @@ To prevent automated agents from causing directory and code regressions, the fol > [!WARNING] > Never push conflict resolutions without running the full test suite. > * If the workspace previously had $N$ passing tests, the resolved branch must have at least $N$ passing tests. -> * Confirm all files staged for deletion or addition are intentional using `git diff --stat origin/master`. +> * Confirm all files staged for deletion, addition, or rename are intentional by running `git diff --name-only origin/master...HEAD` and inspecting the content diffs of renamed or moved files. ### Rule 3: File Integrity & Verification Checklist Add a post-conflict verification script step to the agent workflow: @@ -76,6 +76,6 @@ You are resolving merge conflicts for the branch [branch_name]. 1. Run `git fetch origin` and `git rebase origin/master`. 2. If conflicts arise, inspect whether any files were renamed/moved in master. Apply your changes to the renamed files in their new locations, rather than re-creating them at their old locations. 3. Once rebased, run the entire test suite: `pytest`. -4. Run `git diff --stat origin/master` and review every file addition/deletion. Verify that no tests or scripts are missing compared to master. -5. If any test files or core logic sections are missing, checkout the missing files from master and apply changes cleanly. +4. Run `git diff --name-only origin/master...HEAD` and inspect the full content diff for renamed, deleted, or restored files. +5. Only restore files from `master` after confirming the deletion was accidental; do not auto-checkout files that may have been intentionally renamed or removed. ``` diff --git a/README.md b/README.md index 2d729763..6dd63ede 100644 --- a/README.md +++ b/README.md @@ -795,10 +795,9 @@ For auto-routing modes, the Triage Router handles failures by silently falling b The repository includes an automated integration script to test the 5-tier intent routing pipeline on the live gateway stack: * **Location**: [test_reasoning_tiers.py](scripts/verification/test_reasoning_tiers.py) -This script sends five sequential chat completion requests (from simple to advanced prompt complexities) to the gateway's `llm-routing-auto-free` auto-triage route, verifying that: -1. The local classifier correctly categorizes and labels the prompt intent. -2. The gateway successfully routes the prompt to the mapped LiteLLM fallback model group or provider. -3. The responses are returned successfully with acceptable latency. +This script acts as an end-to-end routing smoke test by sending five sequential chat completion requests (from simple to advanced prompt complexities) to the gateway's `llm-routing-auto-free` auto-triage route, verifying that: +1. The gateway successfully routes the prompt to the expected LiteLLM model group or provider. +2. The responses are returned successfully with acceptable latency. ### How to Run diff --git a/pod.yaml b/pod.yaml index 1f8ed291..5dd54f07 100644 --- a/pod.yaml +++ b/pod.yaml @@ -157,6 +157,8 @@ spec: value: POSTGRES_PASSWORD_RAW_PLACEHOLDER - name: POSTGRES_DB value: langfuse + - name: PGDATA + value: /var/lib/postgresql/data/pg18 image: docker.io/pgvector/pgvector:0.8.3-pg18 livenessProbe: exec: diff --git a/router/memory_mcp.py b/router/memory_mcp.py index a1d33c8d..daaefcea 100755 --- a/router/memory_mcp.py +++ b/router/memory_mcp.py @@ -252,7 +252,7 @@ async def delete_item(client, entry, sem): return None else: return r.text - except Exception as e: + except httpx.HTTPError as e: return str(e) sem = asyncio.Semaphore(10) diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py index 70f68442..22d86aa7 100644 --- a/scripts/benchmark_classifier.py +++ b/scripts/benchmark_classifier.py @@ -63,7 +63,7 @@ def process_item(item): prompt = item["prompt"] expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") predicted = classify(prompt) - except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, TypeError) as e: + except Exception as e: expected = "" if isinstance(item, dict): expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "") diff --git a/start-stack.sh b/start-stack.sh index 3b01be94..4ab0f1e6 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -36,6 +36,9 @@ if [ $# -gt 1 ]; then exit 1 fi +# Ensure local volume directories exist on the host for Podman mounts +mkdir -p valkey-data postgres-data langfuse-data clickhouse-data redis-lf-data minio-data + FULL_REBUILD=false REPLACE_MODE=false if [ "${1:-}" = "--full-rebuild" ]; then @@ -51,8 +54,7 @@ elif [ -n "${1:-}" ]; then exit 1 fi -# Ensure local volume directories exist on the host for Podman mounts -mkdir -p valkey-data postgres-data langfuse-data clickhouse-data redis-lf-data minio-data + ENV_FILE="${WORKDIR}/.env" @@ -80,9 +82,17 @@ fi if [ -z "$OPENROUTER_API_KEY" ]; then if [ -t 0 ]; then echo "🔑 OpenRouter API Key not found." - echo -n "Please enter your OpenRouter API Key (input will be hidden): " - read -rs OPENROUTER_API_KEY - echo "" + while [ -z "$OPENROUTER_API_KEY" ]; do + echo -n "Please enter your OpenRouter API Key (input will be hidden): " + if ! read -rs OPENROUTER_API_KEY; then + echo -e "\n❌ Error: Failed to read OpenRouter API Key (EOF reached). Aborting." >&2 + exit 1 + fi + echo "" + if [ -z "$OPENROUTER_API_KEY" ]; then + echo "❌ Error: API key cannot be empty. Please try again." + fi + done escaped_key=$(escape_env_val "$OPENROUTER_API_KEY") echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE" chmod 600 "$ENV_FILE" @@ -249,7 +259,10 @@ if [ -z "$OLLAMA_API_KEY" ]; then echo "🔑 OLLAMA_API_KEY not found." while [ -z "$OLLAMA_API_KEY" ]; do echo -n "Please enter your Ollama API Key (input will be hidden): " - read -rs OLLAMA_API_KEY + if ! read -rs OLLAMA_API_KEY; then + echo -e "\n❌ Error: Failed to read Ollama API Key (EOF reached). Aborting." >&2 + exit 1 + fi echo "" if [ -z "$OLLAMA_API_KEY" ]; then echo "❌ Error: API key cannot be empty. Please try again." @@ -378,7 +391,10 @@ setup_minio_buckets() { # Ensure mc alias points to the correct MinIO S3 API port (9002, not 9000) # The default 'local' alias in the MinIO image points to :9000 which is ClickHouse, # not MinIO. We must override it. - podman exec agent-router-pod-minio-s3 mc alias set local http://127.0.0.1:9002 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" 2>/dev/null + if ! podman exec agent-router-pod-minio-s3 mc alias set local http://127.0.0.1:9002 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; then + echo "❌ Error: Failed to set MinIO alias 'local' on http://127.0.0.1:9002" >&2 + exit 1 + fi # Create required buckets (idempotent) local BUCKETS=("langfuse-events" "proj-triage-gateway-id") @@ -531,6 +547,15 @@ text = text.replace("MINIO_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ["MINIO_ text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ["LANGFUSE_INIT_USER_PASSWORD"])) text = text.replace("REDIS_AUTH_PLACEHOLDER", yaml_scalar(os.environ["REDIS_AUTH"])) text = text.replace("CLICKHOUSE_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ["CLICKHOUSE_PASSWORD"])) +import re +unresolved = sorted(set(re.findall(r"\b[A-Z0-9_]+_PLACEHOLDER\b", text))) +if unresolved: + sys.stderr.write( + "Error: Unresolved placeholders remain in rendered pod.yaml: " + + ", ".join(unresolved) + + "\n" + ) + sys.exit(1) sys.stdout.write(text) PY } diff --git a/tests/test_host_agy_daemon.py b/tests/test_host_agy_daemon.py index 86600be9..52451dda 100644 --- a/tests/test_host_agy_daemon.py +++ b/tests/test_host_agy_daemon.py @@ -78,9 +78,10 @@ def test_daemon_post_404(daemon_server): def test_daemon_post_stream_false(daemon_server, monkeypatch): req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False, "conversation_id": "conv_abc", "model_override": "gpt-4"}) + captured = {} async def mock_exec(*args, **kwargs): - assert args == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_abc", "--print", "test prompt") - assert kwargs.get("env", {}).get("CASCADE_DEFAULT_MODEL_OVERRIDE") == "gpt-4" + captured["args"] = args + captured["env"] = kwargs.get("env", {}) mock_proc = AsyncMock() mock_proc.returncode = 0 mock_proc.wait = AsyncMock() @@ -100,6 +101,8 @@ async def mock_exec(*args, **kwargs): with urllib.request.urlopen(req) as resp: data = json.loads(resp.read().decode()) + assert captured.get("args") == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_abc", "--print", "test prompt") + assert captured.get("env", {}).get("CASCADE_DEFAULT_MODEL_OVERRIDE") == "gpt-4" assert data["returncode"] == 0 assert data["stdout"] == "mocked stdout output" assert data["stderr"] == "mocked stderr output" @@ -130,9 +133,10 @@ async def slow_wait(): def test_daemon_post_stream_true(daemon_server, monkeypatch): req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True, "model_override": "test-model"}) + captured = {} async def mock_exec(*args, **kwargs): - assert args == (host_agy_daemon.AGY_BINARY, "--print", "test prompt") - assert kwargs.get("env", {}).get("CASCADE_DEFAULT_MODEL_OVERRIDE") == "test-model" + captured["args"] = args + captured["env"] = kwargs.get("env", {}) mock_proc = AsyncMock() mock_proc.returncode = 0 mock_proc.wait = AsyncMock() @@ -158,6 +162,8 @@ def mock_read(fd, n): content = resp.read().decode().strip() lines = content.split("\n") + assert captured.get("args") == (host_agy_daemon.AGY_BINARY, "--print", "test prompt") + assert captured.get("env", {}).get("CASCADE_DEFAULT_MODEL_OVERRIDE") == "test-model" assert len(lines) == 3 assert json.loads(lines[0]) == {"type": "token", "content": "token1\n"} assert json.loads(lines[1]) == {"type": "token", "content": "token2\n"} @@ -227,9 +233,11 @@ def mock_serve_forever(self): # Track if server_close was called close_called = False + real_close = host_agy_daemon.ThreadingHTTPServer.server_close def mock_server_close(self): nonlocal close_called close_called = True + real_close(self) monkeypatch.setattr(host_agy_daemon.ThreadingHTTPServer, "server_close", mock_server_close) @@ -240,8 +248,9 @@ def mock_server_close(self): def test_daemon_post_stream_false_no_model_override(daemon_server, monkeypatch): req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": False}) + captured = {} async def mock_exec(*args, **kwargs): - assert "CASCADE_DEFAULT_MODEL_OVERRIDE" not in kwargs.get("env", {}) + captured["env"] = kwargs.get("env", {}) mock_proc = AsyncMock() mock_proc.returncode = 0 mock_proc.wait = AsyncMock() @@ -253,6 +262,7 @@ async def mock_exec(*args, **kwargs): with urllib.request.urlopen(req) as resp: data = json.loads(resp.read().decode()) + assert "CASCADE_DEFAULT_MODEL_OVERRIDE" not in captured.get("env", {}) assert data["returncode"] == 0 def test_daemon_post_stream_true_read_oserror(daemon_server, monkeypatch): @@ -412,8 +422,9 @@ def mock_unlink(path): def test_daemon_post_stream_true_with_conversation(daemon_server, monkeypatch): req = make_run_request(daemon_server, {"prompt": "test prompt", "stream": True, "conversation_id": "conv_789"}) + captured = {} async def mock_exec(*args, **kwargs): - assert args == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_789", "--print", "test prompt") + captured["args"] = args mock_proc = AsyncMock() mock_proc.returncode = 0 mock_proc.wait = AsyncMock() @@ -427,5 +438,6 @@ async def mock_exec(*args, **kwargs): content = resp.read().decode().strip() lines = content.split("\n") + assert captured.get("args") == (host_agy_daemon.AGY_BINARY, "--conversation", "conv_789", "--print", "test prompt") assert len(lines) == 1 assert json.loads(lines[0]) == {"type": "status", "returncode": 0, "conversation_id": "conv_789"}