Skip to content
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ Navigate your web browser to:
👉 **`http://localhost:5000/dashboard`**

The triage router hosts a beautiful, single-pane-of-glass **Glassmorphic Status Control Panel** styled with modern vanilla CSS featuring:
* **System Status Healthchecks**: Live connection status checks via TCP sockets (Valkey, Postgres) and HTTP pings (LiteLLM, Llama-server).
* **System Status Healthchecks**: Live connection status checks via TCP sockets (Valkey, Postgres), HTTP pings (LiteLLM, Llama-server), and non-blocking asynchronous checks for Gemini OAuth token validation status.
* **Real-time Routing Metrics**: Active classification splits (simple vs complex), request logs, and processing latencies.
* **Direct Application Portals**: One-click navigation links to target web utilities (LiteLLM administration console, Langfuse telemetry console, Llama-Server playground).

Expand Down Expand Up @@ -571,6 +571,9 @@ Without Minio, Langfuse v3 **will not start** — it validates S3 connectivity a

### Configuration

> [!WARNING]
> The credentials `minioadmin` / `minioadmin` and settings specified below are strictly for **local development and testing**. You **must** override these default credentials before any shared, staging, or production use.

| Env Var | Value |
|----------|-------|
| `LANGFUSE_S3_EVENT_UPLOAD_BUCKET` | `langfuse-events` |
Expand All @@ -579,12 +582,15 @@ Without Minio, Langfuse v3 **will not start** — it validates S3 connectivity a
| `LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY` | `minioadmin` |
| `S3_FORCE_PATH_STYLE` | `true` |

Minio runs on ports **9001** (web console) and **9002** (S3 API). Credentials: `minioadmin` / `minioadmin`. Image pinned to `docker.io/minio/minio:RELEASE.2025-10-15T17-29-55Z`.
Minio runs on ports **9001** (web console) and **9002** (S3 API). Default credentials (`minioadmin` / `minioadmin`) are only meant for local/dev setups. Image pinned to `docker.io/minio/minio:RELEASE.2025-10-15T17-29-55Z`.

### Health Check

MinIO's health is monitored using its native structured endpoints `/minio/health/live` (liveness) and `/minio/health/ready` (readiness) on port 9002:

> [!IMPORTANT]
> When deploying to staging or production, ensure that custom credentials are configured (rather than the default `minioadmin` keys described in [Configuration](#configuration)), and ensure the deployment's probes and S3 configurations are updated to reference the new values.

```yaml
livenessProbe:
httpGet:
Expand Down
57 changes: 31 additions & 26 deletions router/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"/"
)

GEMINI_OAUTH_CREDS_PATH = "/config/gemini_auth/oauth_creds.json"


_redis_client = None
_redis_last_init_attempt = 0.0
Expand Down Expand Up @@ -1027,45 +1029,48 @@ async def classify_request(
return "agent-advanced-core", latency, False, "advanced (exception)"


def get_live_gemini_oauth_token() -> str | None:
def _read_json_file_sync(file_path: str) -> dict:
"""Helper to read JSON files synchronously."""
with open(file_path, "r") as f:
return json.load(f)

async def get_live_gemini_oauth_token() -> str | None:
"""Retrieve the current valid Gemini OAuth access token from local storage if not expired."""
try:
creds_path = "/config/gemini_auth/oauth_creds.json"
if os.path.exists(creds_path):
with open(creds_path, "r") as f:
data = json.load(f)
access_token = data.get("access_token")
expiry_ms = data.get("expiry_date", 0)
# Convert current time to milliseconds
current_ms = int(time.time() * 1000)
if access_token and current_ms < expiry_ms:
logger.info(
"🔑 Found valid, unexpired Gemini OAuth token from host!"
)
return access_token
else:
# agy CLI uses the OS system keyring (GNOME Keyring), not this
# stale disk file. The file being expired is expected — don't warn.
logger.debug(
"Gemini OAuth token on disk is expired — agy uses system keyring instead."
)
if await asyncio.to_thread(os.path.exists, GEMINI_OAUTH_CREDS_PATH):
data = await asyncio.to_thread(_read_json_file_sync, GEMINI_OAUTH_CREDS_PATH)
access_token = data.get("access_token")
expiry_ms = data.get("expiry_date", 0)
# Convert current time to milliseconds
current_ms = int(time.time() * 1000)
if access_token and current_ms < expiry_ms:
logger.info(
"🔑 Found valid, unexpired Gemini OAuth token from host!"
)
return access_token
else:
# agy CLI uses the OS system keyring (GNOME Keyring), not this
# stale disk file. The file being expired is expected — don't warn.
logger.debug(
"Gemini OAuth token on disk is expired — agy uses system keyring instead."
)
except Exception as e:
logger.error(f"Failed to read live OAuth token: {e}")
return None


def get_gemini_oauth_status() -> dict:


async def get_gemini_oauth_status() -> dict:
"""Returns structured OAuth status for the dashboard banner."""
creds_path = "/config/gemini_auth/oauth_creds.json"
try:
if not os.path.exists(creds_path):
if not await asyncio.to_thread(os.path.exists, GEMINI_OAUTH_CREDS_PATH):
return {
"status": "missing",
"detail": "No oauth_creds.json found",
"expiry_ms": 0,
}
with open(creds_path, "r") as f:
data = json.load(f)
data = await asyncio.to_thread(_read_json_file_sync, GEMINI_OAUTH_CREDS_PATH)
access_token = data.get("access_token")
expiry_ms = data.get("expiry_date", 0)
current_ms = int(time.time() * 1000)
Expand Down Expand Up @@ -2626,7 +2631,7 @@ async def get_dashboard_data():
check_http_endpoint("http://127.0.0.1:4000/"),
check_http_endpoint("http://127.0.0.1:8080/health"),
check_http_endpoint("http://127.0.0.1:3001"),
asyncio.to_thread(get_gemini_oauth_status),
get_gemini_oauth_status(),
asyncio.wait_for(get_best_free_model(), timeout=5.0),
asyncio.to_thread(get_goose_sessions),
asyncio.wait_for(get_llamacpp_metrics(), timeout=5.0),
Expand Down
2 changes: 1 addition & 1 deletion router/tests/test_dashboard_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async def test_get_dashboard_data_structure():
with patch("main.sync_cooldowns_from_valkey", new_callable=AsyncMock) as mock_sync, \
patch("main.check_tcp_port", new_callable=AsyncMock) as mock_tcp, \
patch("main.check_http_endpoint", new_callable=AsyncMock) as mock_http, \
patch("main.get_gemini_oauth_status") as mock_oauth, \
patch("main.get_gemini_oauth_status", new_callable=AsyncMock) as mock_oauth, \
patch("main.get_best_free_model", new_callable=AsyncMock) as mock_best_model, \
patch("main.get_goose_sessions") as mock_goose, \
patch("main.get_llamacpp_metrics", new_callable=AsyncMock) as mock_llamacpp, \
Expand Down
80 changes: 80 additions & 0 deletions router/tests/test_get_live_gemini_oauth_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import sys
import json
import time
import pytest
from unittest.mock import patch, mock_open

# Add router to sys.path properly
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)

with patch('builtins.open', mock_open(read_data='''
server:
host: 0.0.0.0
port: 5000
router:
router_model:
api_key: "dummy_key"
model: "dummy_model"
''')):
import main

@pytest.mark.asyncio
async def test_get_live_gemini_oauth_token_valid():
"""Test retrieving a valid, unexpired token."""
mock_data = {
"access_token": "valid_token_123",
"expiry_date": int(time.time() * 1000) + 3600000 # 1 hour in the future
}
with patch("os.path.exists", return_value=True), \
patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \
patch("main.logger.info") as mock_logger:
token = await main.get_live_gemini_oauth_token()
assert token == "valid_token_123"
mock_logger.assert_called_with("🔑 Found valid, unexpired Gemini OAuth token from host!")

@pytest.mark.asyncio
async def test_get_live_gemini_oauth_token_expired():
"""Test that an expired token returns None."""
mock_data = {
"access_token": "expired_token_456",
"expiry_date": int(time.time() * 1000) - 3600000 # 1 hour in the past
}
with patch("os.path.exists", return_value=True), \
patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \
patch("main.logger.debug") as mock_logger:
token = await main.get_live_gemini_oauth_token()
assert token is None
mock_logger.assert_called_with("Gemini OAuth token on disk is expired — agy uses system keyring instead.")

@pytest.mark.asyncio
async def test_get_live_gemini_oauth_token_missing_file():
"""Test behavior when the credentials file does not exist."""
with patch("os.path.exists", return_value=False):
token = await main.get_live_gemini_oauth_token()
assert token is None

@pytest.mark.asyncio
async def test_get_live_gemini_oauth_token_invalid_json():
"""Test behavior when the credentials file contains invalid JSON."""
with patch("os.path.exists", return_value=True), \
patch("builtins.open", mock_open(read_data="invalid json")), \
patch("main.logger.error") as mock_logger:
token = await main.get_live_gemini_oauth_token()
assert token is None
mock_logger.assert_called()

@pytest.mark.asyncio
async def test_get_live_gemini_oauth_token_missing_access_token():
"""Test behavior when the JSON does not contain an access_token."""
mock_data = {
"expiry_date": int(time.time() * 1000) + 3600000
}
with patch("os.path.exists", return_value=True), \
patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \
patch("main.logger.debug") as mock_logger:
token = await main.get_live_gemini_oauth_token()
assert token is None
mock_logger.assert_called_with("Gemini OAuth token on disk is expired — agy uses system keyring instead.")
24 changes: 21 additions & 3 deletions test_record_tool_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,20 @@ def test_record_tool_usage_none_mapping():

def test_record_tool_usage_accumulation():
"""Test that tokens accumulate correctly over multiple calls."""
router.main.record_tool_usage(ToolUsageRecord("write", 10, 10, "model1", 50.0))
router.main.record_tool_usage(ToolUsageRecord("write", 20, 30, "model2", 60.0))
router.main.record_tool_usage(ToolUsageRecord(
tool_name="write",
prompt_tokens=10,
completion_tokens=10,
model="model1",
latency_ms=50.0
))
router.main.record_tool_usage(ToolUsageRecord(
tool_name="write",
prompt_tokens=20,
completion_tokens=30,
model="model2",
latency_ms=60.0
))

assert router.main.stats["tool_tokens"]["write"] == 70
assert router.main.stats["prompt_tokens"] == 30
Expand All @@ -71,7 +83,13 @@ def test_record_tool_usage_timeline_limit():
"""Test that the timeline buffer is capped at 15 events."""
# Add 20 events
for i in range(20):
router.main.record_tool_usage(ToolUsageRecord(f"tool_{i}", 1, 1, "model", 10.0))
router.main.record_tool_usage(ToolUsageRecord(
tool_name=f"tool_{i}",
prompt_tokens=1,
completion_tokens=1,
model="model",
latency_ms=10.0
))

assert len(router.main.stats["timeline"]) == 15
# The first 5 events should be popped off, so the oldest event in the timeline
Expand Down