Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ed78fa7
test: add tests for _read_annotations_sync cache logic
google-labs-jules[bot] Jun 30, 2026
92bd976
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jun 30, 2026
c6e3c10
test: add tests for _read_annotations_sync cache logic
google-labs-jules[bot] Jun 30, 2026
64248a0
chore: merge master to resolve conflicts
google-labs-jules[bot] Jul 1, 2026
a53d15f
chore: resolved merge conflicts with master
google-labs-jules[bot] Jul 1, 2026
5ae5f08
chore: resolved merge conflict with renamed directory
google-labs-jules[bot] Jul 1, 2026
55b879e
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
35d1e54
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
89ea4aa
chore: move test file to tests/ directory
google-labs-jules[bot] Jul 1, 2026
7846636
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
87a6587
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
8607273
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
e415dd0
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
c067cd6
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
01206d9
fix: resolve ImportError by importing router.main module for cache ac…
google-labs-jules[bot] Jul 1, 2026
c328562
Merge branch 'master' into test/add-read-annotations-sync-tests-12043…
sheepdestroyer Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ All configurations, automation scripts, and databases are self-contained within

```
/home/gpav/Vrac/LAB/AI/LLM-Routing/
├── .env # Environment file for API keys, passwords, and generated secrets (ignored by git)
├── .env # Environment file for OpenRouter API Key (ignored by git)
├── .gitignore # Git ignore policy protecting secrets & database files
├── README.md # In-depth system and operational guide
├── pod.yaml # Podman Kubernetes template defining the 10-container stack
Expand Down
16 changes: 8 additions & 8 deletions pod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.5.1
livenessProbe:
exec:
Expand All @@ -196,7 +196,7 @@ spec:
- --user
- clickhouse
- --password
- clickhouse
- CLICKHOUSE_PASSWORD_PLACEHOLDER
- --query
- SELECT 1
initialDelaySeconds: 20
Expand All @@ -222,7 +222,7 @@ spec:
- --port
- '6380'
- --requirepass
- langfuse-redis-2026
- REDIS_AUTH_PLACEHOLDER
- --maxmemory-policy
- noeviction
- --loglevel
Expand All @@ -242,7 +242,7 @@ spec:
- -p
- "6380"
- -a
- langfuse-redis-2026
- REDIS_AUTH_PLACEHOLDER
- ping
initialDelaySeconds: 2
periodSeconds: 5
Expand Down Expand Up @@ -287,15 +287,15 @@ spec:
- name: CLICKHOUSE_USER
value: clickhouse
- name: CLICKHOUSE_PASSWORD
value: clickhouse
value: CLICKHOUSE_PASSWORD_PLACEHOLDER
- name: CLICKHOUSE_CLUSTER_ENABLED
value: 'false'
- name: REDIS_HOST
value: 127.0.0.1
- 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
Expand Down Expand Up @@ -343,15 +343,15 @@ spec:
- name: CLICKHOUSE_USER
value: clickhouse
- name: CLICKHOUSE_PASSWORD
value: clickhouse
value: CLICKHOUSE_PASSWORD_PLACEHOLDER
- name: CLICKHOUSE_CLUSTER_ENABLED
value: 'false'
- name: REDIS_HOST
value: 127.0.0.1
- 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
Expand Down
2 changes: 1 addition & 1 deletion router/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 aiofiles
RUN pip install --no-cache-dir fastapi "pydantic>=2.0,<3.0" uvicorn httpx pyyaml python-multipart asyncpg langfuse redis

# 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/
Expand Down
34 changes: 21 additions & 13 deletions router/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,10 @@ def get_redis():
return None
_redis_last_init_attempt = now
try:
url = os.getenv("VALKEY_URL")
if url:
_redis_client = aioredis.Redis.from_url(url, decode_responses=True, socket_timeout=1.0)
logger.info(f"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}")
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
Expand Down Expand Up @@ -1452,8 +1447,8 @@ async def proxy_models():
{"id": "llm-routing-agy", "object": "model", "created": 0, "owned_by": "llm-routing", "context_length": 1048576},
{"id": "llm-routing-ollama", "object": "model", "created": 0, "owned_by": "llm-routing", "context_length": 524288},
]
for entry in reversed(routing_models):
data["data"].insert(0, entry)
data["data"] = routing_models + data["data"]

return JSONResponse(content=data, status_code=200)
except Exception as parse_err:
logger.warning(f"Failed to parse /v1/models JSON despite status 200: {parse_err}")
Expand Down Expand Up @@ -3382,10 +3377,23 @@ class AnnotationItem(BaseModel):
# the atomic file-replace mechanism, which is acceptable for this dashboard feature.
annotations_lock = asyncio.Lock()

_annotations_cache = {}

def _read_annotations_sync(path) -> dict:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
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 = os.path.getmtime(path)

cache_entry = _annotations_cache.get(path)

if cache_entry is None or current_mtime != cache_entry["mtime"]:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
_annotations_cache[path] = {"mtime": current_mtime, "data": data}

return copy.deepcopy(_annotations_cache[path]["data"])

@app.post("/dashboard/save-annotations")
async def save_annotations(payload: Dict[str, AnnotationItem]):
Expand Down
46 changes: 0 additions & 46 deletions router/tests/test_get_goose_sessions.py

This file was deleted.

35 changes: 0 additions & 35 deletions router/tests/test_get_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,38 +99,3 @@ def test_get_redis_initialization_exception(mock_logger_warning, mock_redis, moc
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()
73 changes: 22 additions & 51 deletions scripts/benchmark_classifier.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Benchmark gemma4-26a4b-routing classifier against labeled dataset."""
import os
import concurrent.futures
import threading
import json, urllib.request, time, sys
from collections import defaultdict, Counter
from pathlib import Path
Expand Down Expand Up @@ -56,8 +54,7 @@ def classify(prompt):
per_tier = {t: {"correct": 0, "total": 0} for t in TIERS}
confusion = defaultdict(Counter) # confusion[expected][predicted]


def process_item(item):
for i, item in enumerate(dataset.get("prompts", [])):
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", "")
Expand All @@ -67,57 +64,31 @@ def process_item(item):
except Exception as e:
predicted = f"ERROR: {str(e)[:50]}"

return expected, predicted

results_list = [None] * total
results.append({
"prompt": prompt[:100],
"expected": expected,
"predicted": predicted,
})

with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
# Submit all tasks immediately, but executor map will process them and yield results in order
# To maintain rate limit properly without the quadratic delay or blocking progress, we can use a threading.Lock
rate_limit_lock = threading.Lock()

def process_item_with_rate_limit(index_and_item):
i, item = index_and_item
# Enforce rate limit globally across all threads
with rate_limit_lock:
time.sleep(0.05)
# Only score against known tiers — skip ERROR/unknown labels gracefully
if expected not in per_tier:
confusion[expected][predicted] += 1
continue

expected, predicted = process_item(item)
return i, item, expected, predicted
per_tier[expected]["total"] += 1
if predicted == expected:
correct += 1
per_tier[expected]["correct"] += 1

# We map over items, but map blocks if we process in order. We can use as_completed and store by index to preserve order.
futures = [executor.submit(process_item_with_rate_limit, (i, item)) for i, item in enumerate(dataset.get("prompts", []))]
confusion[expected][predicted] += 1

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,
}

# Only score against known tiers — skip ERROR/unknown labels gracefully
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

# Progress
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}%")

# Filter out Nones if any
results = [r for r in results_list if r is not None]
# 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}%")

time.sleep(0.05) # minimal rate-limit (model handles concurrency via llama-server slots)

# Report
scored_total = sum(t["total"] for t in per_tier.values())
Expand Down
24 changes: 21 additions & 3 deletions start-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if [ -f "$ENV_FILE" ]; then
set +a
fi


# Ensure openssl is installed if we need to generate passwords/keys


if [ -z "$OPENROUTER_API_KEY" ]; then
Expand Down Expand Up @@ -141,6 +141,18 @@ if [ -z "$LANGFUSE_INIT_USER_PASSWORD" ]; then
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)"
Expand All @@ -160,6 +172,8 @@ if [ -z "$MINIO_ROOT_PASSWORD" ]; then
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

FULL_REBUILD=false
Expand Down Expand Up @@ -368,7 +382,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 REDIS_AUTH CLICKHOUSE_PASSWORD
python3 - "$WORKDIR/pod.yaml" <<'PY'
import os, sys, urllib.parse
uid = os.getuid()
Expand All @@ -386,7 +400,9 @@ placeholders = [
"postgres-password-***",
"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:
Expand All @@ -406,6 +422,8 @@ 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"])
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
}
Expand Down
Loading