diff --git a/.env.dev b/.env.dev new file mode 100644 index 00000000..a5393f15 --- /dev/null +++ b/.env.dev @@ -0,0 +1,27 @@ +# Dev environment overlay โ€” sourced AFTER .env to override prod values +# Secrets are inherited from .env; only env-specific config is set here. + +# Pod identity +POD_NAME="dev-router-pod" + +# Public URLs +BASEURL="dev.vendeuvre.lan" +BASE_URL="dev.vendeuvre.lan" +PUBLIC_BASE_URL="https://dev.vendeuvre.lan/llm-routing" + +# Dev port assignments (+10 offset from production) +ROUTER_PORT="5010" +LITELLM_PORT="4010" +LANGFUSE_WEB_PORT="3011" +LANGFUSE_WORKER_PORT="3040" +POSTGRES_PORT="5442" +VALKEY_CACHE_PORT="6389" +VALKEY_LF_PORT="6390" +CLICKHOUSE_HTTP_PORT="8133" +CLICKHOUSE_TCP_PORT="9010" +CLICKHOUSE_INTERSERVER_PORT="9019" +MINIO_S3_PORT="9012" +MINIO_CONSOLE_PORT="9011" + +# Dev uses locally-built router image (for testing code changes) +ROUTER_IMAGE="localhost/llm-routing-dev:latest" diff --git a/.gitignore b/.gitignore index 5ccf9013..41daa843 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,9 @@ # Exclude Podman runtime file generated dynamically at startup pod-run.yaml -# Exclude persistent database and cache storages -postgres-data/ -langfuse-data/ -valkey-data/ -clickhouse-data/ -minio-data/ -redis-lf-data/ +# Exclude persistent database and cache storages (all under data/) +data/ + # Exclude auto-generated backups backups/ @@ -36,7 +32,5 @@ router/free_models_roster.json workdirs/ pr_description.txt -# Dataset work in progress -data/ .cache/ test_output*.log diff --git a/litellm/config.yaml b/litellm/config.yaml index 195712f5..b204f9cc 100644 --- a/litellm/config.yaml +++ b/litellm/config.yaml @@ -12,7 +12,7 @@ litellm_settings: cache_params: host: 127.0.0.1 mode: default - port: 6379 + port: VALKEY_CACHE_PORT_PLACEHOLDER ttl: 3600 type: redis caching_backend: redis @@ -77,7 +77,7 @@ model_list: is_public_model_group: true - litellm_params: model: openai/llm-routing-ollama - api_base: http://127.0.0.1:5000/v1 + api_base: http://127.0.0.1:ROUTER_PORT_PLACEHOLDER/v1 api_key: os.environ/LITELLM_MASTER_KEY request_timeout: 120 model_name: llm-routing-ollama @@ -237,7 +237,7 @@ model_list: redis_settings: redis_host: 127.0.0.1 - redis_port: 6379 + redis_port: VALKEY_CACHE_PORT_PLACEHOLDER router_settings: allowed_fails: 0 cooldown_time: 300 diff --git a/litellm/entrypoint.py b/litellm/entrypoint.py index 65520978..b8755d44 100644 --- a/litellm/entrypoint.py +++ b/litellm/entrypoint.py @@ -17,7 +17,7 @@ if line and not line.startswith("#") and "=" in line: key, _, val = line.partition("=") val = val.strip().strip('"').strip("'") - os.environ[key] = val + os.environ.setdefault(key, val) # Load Gemini OAuth token from credentials JSON creds_path = "/config/gemini_auth/oauth_creds.json" @@ -46,9 +46,10 @@ def check_tcp_port(ip: str, port: int) -> bool: return False max_wait = 60 -print(f"๐Ÿ”Œ Waiting for PostgreSQL on :5432 (max {max_wait}s)...") +postgres_port = int(os.environ.get("POSTGRES_PORT", "5432")) +print(f"๐Ÿ”Œ Waiting for PostgreSQL on :{postgres_port} (max {max_wait}s)...") for i in range(max_wait): - if check_tcp_port("127.0.0.1", 5432): + if check_tcp_port("127.0.0.1", postgres_port): print(f"โœ… PostgreSQL ready after {i+1}s") break time.sleep(1) @@ -127,5 +128,6 @@ def _serialize_dt(dt): # Start LiteLLM Proxy import litellm from litellm.proxy.proxy_cli import run_server -sys.argv = ["litellm", "--config", "/app/config.yaml", "--port", "4000"] +litellm_port = os.environ.get("LITELLM_PORT", os.environ.get("PORT", "4000")) +sys.argv = ["litellm", "--config", "/app/config.yaml", "--port", litellm_port] run_server() diff --git a/pod.yaml b/pod.yaml index a76bfcbb..c871aabf 100644 --- a/pod.yaml +++ b/pod.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: agent-router-pod + name: POD_NAME_PLACEHOLDER spec: containers: # Valkey Cache โ€” LiteLLM response cache (exact-match & semantic) @@ -9,19 +9,29 @@ spec: - valkey-server - --bind - 127.0.0.1 + - --port + - 'VALKEY_CACHE_PORT_PLACEHOLDER' - --loglevel - warning - image: docker.io/valkey/valkey:9.1.0-alpine + image: docker.io/valkey/valkey:9.1.0 livenessProbe: - tcpSocket: - port: 6379 + exec: + command: + - valkey-cli + - -p + - 'VALKEY_CACHE_PORT_PLACEHOLDER' + - ping initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 name: valkey-cache readinessProbe: - tcpSocket: - port: 6379 + exec: + command: + - valkey-cli + - -p + - 'VALKEY_CACHE_PORT_PLACEHOLDER' + - ping initialDelaySeconds: 2 periodSeconds: 5 timeoutSeconds: 2 @@ -34,28 +44,34 @@ spec: - /app/entrypoint.py env: - name: DATABASE_URL - value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/postgres + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:POSTGRES_PORT_PLACEHOLDER/postgres - name: STORE_MODEL_IN_DB value: 'True' - name: LITELLM_LOG value: INFO - name: LANGFUSE_HOST - value: http://127.0.0.1:3001 + value: http://127.0.0.1:LANGFUSE_WEB_PORT_PLACEHOLDER - name: LITELLM_MASTER_KEY value: LITELLM_MASTER_KEY_PLACEHOLDER - name: OLLAMA_API_KEY value: OLLAMA_API_KEY_PLACEHOLDER - name: SERVER_ROOT_PATH - value: SERVER_ROOT_PATH_PLACEHOLDER + value: /llm-routing/litellm - name: PROXY_BASE_URL value: PROXY_BASE_URL_PLACEHOLDER + - name: PORT + value: 'LITELLM_PORT_PLACEHOLDER' + - name: LITELLM_PORT + value: 'LITELLM_PORT_PLACEHOLDER' + - name: POSTGRES_PORT + value: 'POSTGRES_PORT_PLACEHOLDER' image: ghcr.io/berriai/litellm:v1.91.1 livenessProbe: exec: command: - python3 - -c - - import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveness') + - import urllib.request; urllib.request.urlopen('http://localhost:LITELLM_PORT_PLACEHOLDER/ping') initialDelaySeconds: 240 periodSeconds: 15 timeoutSeconds: 5 @@ -65,16 +81,16 @@ spec: command: - python3 - -c - - import urllib.request; urllib.request.urlopen('http://localhost:4000/health/readiness') + - import urllib.request; urllib.request.urlopen('http://localhost:LITELLM_PORT_PLACEHOLDER/health/readiness') initialDelaySeconds: 45 periodSeconds: 10 timeoutSeconds: 5 volumeMounts: - mountPath: /app/config.yaml - name: litellm-config + name: litellm-rendered subPath: config.yaml - mountPath: /app/entrypoint.py - name: litellm-config + name: litellm-rendered subPath: entrypoint.py - mountPath: /config/.env name: env-file @@ -83,7 +99,7 @@ spec: name: gemini-secrets # LLM Triage Router โ€” classification + routing gateway - args: - - set -a && . /config/.env && set +a && exec uvicorn main:app --host 0.0.0.0 --port 5000 --log-level ${LOG_LEVEL:-warning} + - set -a && . /config/.env && set +a && LITELLM_PORT=LITELLM_PORT_PLACEHOLDER VALKEY_CACHE_PORT=VALKEY_CACHE_PORT_PLACEHOLDER exec uvicorn main:app --host 0.0.0.0 --port ROUTER_PORT_PLACEHOLDER --log-level ${LOG_LEVEL:-warning} command: - /bin/sh - -c @@ -93,7 +109,7 @@ spec: - name: LITELLM_CONFIG_PATH value: /config/litellm_dir/config.yaml - name: DATABASE_URL - value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/postgres + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:POSTGRES_PORT_PLACEHOLDER/postgres - name: DBUS_SESSION_BUS_ADDRESS value: unix:path=RUN_USER_PLACEHOLDER/bus - name: LITELLM_MASTER_KEY @@ -103,22 +119,26 @@ spec: - name: LANGFUSE_SECRET_KEY value: LANGFUSE_SECRET_KEY_PLACEHOLDER - name: LANGFUSE_HOST - value: http://127.0.0.1:3001 + value: http://127.0.0.1:LANGFUSE_WEB_PORT_PLACEHOLDER - name: OLLAMA_API_KEY value: OLLAMA_API_KEY_PLACEHOLDER - name: PUBLIC_BASE_URL value: PUBLIC_BASE_URL_PLACEHOLDER - name: ROUTING_DOMAIN value: ROUTING_DOMAIN_PLACEHOLDER + - name: LITELLM_PORT + value: 'LITELLM_PORT_PLACEHOLDER' + - name: VALKEY_CACHE_PORT + value: 'VALKEY_CACHE_PORT_PLACEHOLDER' - name: LOG_LEVEL value: info - image: localhost/llm-triage-router:latest + image: ROUTER_IMAGE_PLACEHOLDER livenessProbe: exec: command: - python3 - -c - - import urllib.request; urllib.request.urlopen('http://localhost:5000/metrics') + - import urllib.request; urllib.request.urlopen('http://localhost:ROUTER_PORT_PLACEHOLDER/metrics') initialDelaySeconds: 20 periodSeconds: 15 timeoutSeconds: 5 @@ -128,7 +148,7 @@ spec: command: - python3 - -c - - import urllib.request; urllib.request.urlopen('http://localhost:5000/metrics') + - import urllib.request; urllib.request.urlopen('http://localhost:ROUTER_PORT_PLACEHOLDER/metrics') initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 @@ -136,7 +156,7 @@ spec: - mountPath: /config/router_dir name: router-config - mountPath: /config/litellm_dir - name: litellm-config + name: litellm-rendered - mountPath: /app/data name: dataset-data - mountPath: /config/gemini_auth @@ -167,6 +187,8 @@ spec: value: langfuse - name: PGDATA value: /var/lib/postgresql/data/pg18 + - name: PGPORT + value: 'POSTGRES_PORT_PLACEHOLDER' image: docker.io/pgvector/pgvector:0.8.3-pg18 livenessProbe: exec: @@ -174,6 +196,8 @@ spec: - pg_isready - -U - postgres + - -p + - 'POSTGRES_PORT_PLACEHOLDER' initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 5 @@ -184,6 +208,8 @@ spec: - pg_isready - -U - postgres + - -p + - 'POSTGRES_PORT_PLACEHOLDER' initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 5 @@ -203,6 +229,10 @@ spec: exec: command: - clickhouse-client + - --host + - 127.0.0.1 + - --port + - 'CLICKHOUSE_TCP_PORT_PLACEHOLDER' - --user - clickhouse - --password @@ -217,6 +247,10 @@ spec: exec: command: - clickhouse-client + - --host + - 127.0.0.1 + - --port + - 'CLICKHOUSE_TCP_PORT_PLACEHOLDER' - --user - clickhouse - --password @@ -230,23 +264,32 @@ spec: volumeMounts: - mountPath: /var/lib/clickhouse name: clickhouse-storage + - mountPath: /etc/clickhouse-server/config.d/port-override.xml + name: clickhouse-port-config + subPath: port-override.xml # Valkey LF โ€” Langfuse v3 BullMQ job queue - command: - redis-server - --bind - 127.0.0.1 - --port - - '6380' + - 'VALKEY_LF_PORT_PLACEHOLDER' - --requirepass - REDIS_AUTH_PLACEHOLDER - --maxmemory-policy - noeviction - --loglevel - warning - image: docker.io/valkey/valkey:9.1.0-alpine + image: docker.io/valkey/valkey:9.1.0 livenessProbe: - tcpSocket: - port: 6380 + exec: + command: + - valkey-cli + - -p + - "VALKEY_LF_PORT_PLACEHOLDER" + - -a + - REDIS_AUTH_PLACEHOLDER + - ping initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 @@ -256,7 +299,7 @@ spec: command: - valkey-cli - -p - - "6380" + - "VALKEY_LF_PORT_PLACEHOLDER" - -a - REDIS_AUTH_PLACEHOLDER - ping @@ -269,11 +312,11 @@ spec: # Langfuse Web โ€” observability dashboard - env: - name: DATABASE_URL - value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/langfuse + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:POSTGRES_PORT_PLACEHOLDER/langfuse - name: NEXTAUTH_SECRET value: NEXTAUTH_SECRET_PLACEHOLDER - name: NEXTAUTH_URL - value: NEXTAUTH_URL_PLACEHOLDER + value: http://localhost:LANGFUSE_WEB_PORT_PLACEHOLDER - name: SALT value: SALT_PLACEHOLDER - name: ENCRYPTION_KEY @@ -281,7 +324,7 @@ spec: - name: HOSTNAME value: 0.0.0.0 - name: PORT - value: '3001' + value: 'LANGFUSE_WEB_PORT_PLACEHOLDER' - name: TELEMETRY_ENABLED value: 'false' - name: LANGFUSE_S3_EVENT_UPLOAD_BUCKET @@ -293,13 +336,13 @@ spec: - name: LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY value: MINIO_PASSWORD_PLACEHOLDER - name: LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT - value: http://127.0.0.1:9002 + value: http://127.0.0.1:MINIO_S3_PORT_PLACEHOLDER - name: S3_FORCE_PATH_STYLE value: 'true' - name: CLICKHOUSE_URL - value: http://127.0.0.1:8123 + value: http://127.0.0.1:CLICKHOUSE_HTTP_PORT_PLACEHOLDER - name: CLICKHOUSE_MIGRATION_URL - value: clickhouse://127.0.0.1:9000 + value: clickhouse://127.0.0.1:CLICKHOUSE_TCP_PORT_PLACEHOLDER - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD @@ -309,7 +352,7 @@ spec: - name: REDIS_HOST value: 127.0.0.1 - name: REDIS_PORT - value: '6380' + value: 'VALKEY_LF_PORT_PLACEHOLDER' - name: REDIS_AUTH value: REDIS_AUTH_PLACEHOLDER - name: LANGFUSE_INIT_ORG_ID @@ -334,7 +377,7 @@ spec: - -q - -O - /dev/null - - http://127.0.0.1:3001/api/health + - http://127.0.0.1:LANGFUSE_WEB_PORT_PLACEHOLDER/api/health initialDelaySeconds: 240 periodSeconds: 15 timeoutSeconds: 5 @@ -346,16 +389,16 @@ spec: - -q - -O - /dev/null - - http://127.0.0.1:3001/api/health + - http://127.0.0.1:LANGFUSE_WEB_PORT_PLACEHOLDER/api/health initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 5 # Langfuse Worker โ€” background job processor - env: - name: DATABASE_URL - value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/langfuse + value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:POSTGRES_PORT_PLACEHOLDER/langfuse - name: CLICKHOUSE_URL - value: http://127.0.0.1:8123 + value: http://127.0.0.1:CLICKHOUSE_HTTP_PORT_PLACEHOLDER - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD @@ -365,13 +408,13 @@ spec: - name: REDIS_HOST value: 127.0.0.1 - name: REDIS_PORT - value: '6380' + value: 'VALKEY_LF_PORT_PLACEHOLDER' - name: REDIS_AUTH value: REDIS_AUTH_PLACEHOLDER - name: HOSTNAME value: 0.0.0.0 - name: PORT - value: '3030' + value: 'LANGFUSE_WORKER_PORT_PLACEHOLDER' - name: TELEMETRY_ENABLED value: 'false' - name: LANGFUSE_S3_EVENT_UPLOAD_BUCKET @@ -383,7 +426,7 @@ spec: - name: S3_FORCE_PATH_STYLE value: 'true' - name: LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT - value: http://127.0.0.1:9002 + value: http://127.0.0.1:MINIO_S3_PORT_PLACEHOLDER - name: LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY value: MINIO_PASSWORD_PLACEHOLDER - name: LANGFUSE_LOG_LEVEL @@ -404,9 +447,9 @@ spec: - --quiet - /data - --address - - ":9002" + - ":MINIO_S3_PORT_PLACEHOLDER" - --console-address - - ":9001" + - ":MINIO_CONSOLE_PORT_PLACEHOLDER" env: - name: MINIO_ROOT_USER value: MINIO_USER_PLACEHOLDER @@ -416,7 +459,7 @@ spec: livenessProbe: httpGet: path: /minio/health/live - port: 9002 + port: MINIO_S3_PORT_PLACEHOLDER initialDelaySeconds: 10 periodSeconds: 15 timeoutSeconds: 5 @@ -424,7 +467,7 @@ spec: readinessProbe: httpGet: path: /minio/health/ready - port: 9002 + port: MINIO_S3_PORT_PLACEHOLDER initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 5 @@ -435,25 +478,27 @@ spec: restartPolicy: Always volumes: - hostPath: - path: WORKDIR_PLACEHOLDER/valkey-data + path: DATA_ROOT_PLACEHOLDER/valkey-data name: valkey-storage - hostPath: - path: WORKDIR_PLACEHOLDER/litellm - name: litellm-config + path: DATA_ROOT_PLACEHOLDER/litellm-rendered + type: DirectoryOrCreate + name: litellm-rendered - hostPath: - path: WORKDIR_PLACEHOLDER/router + path: DATA_ROOT_PLACEHOLDER/router-rendered + type: DirectoryOrCreate name: router-config - hostPath: - path: WORKDIR_PLACEHOLDER/postgres-data + path: DATA_ROOT_PLACEHOLDER/postgres-data name: postgres-storage - hostPath: - path: WORKDIR_PLACEHOLDER/clickhouse-data + path: DATA_ROOT_PLACEHOLDER/clickhouse-data name: clickhouse-storage - hostPath: - path: WORKDIR_PLACEHOLDER/redis-lf-data + path: DATA_ROOT_PLACEHOLDER/redis-lf-data name: redis-lf-storage - hostPath: - path: WORKDIR_PLACEHOLDER/minio-data + path: DATA_ROOT_PLACEHOLDER/minio-data name: minio-storage - hostPath: path: HOME_PLACEHOLDER/.gemini @@ -477,6 +522,10 @@ spec: path: WORKDIR_PLACEHOLDER name: env-file - hostPath: - path: WORKDIR_PLACEHOLDER/data + path: DATA_ROOT_PLACEHOLDER/datasets type: DirectoryOrCreate name: dataset-data + - hostPath: + path: DATA_ROOT_PLACEHOLDER/clickhouse-config + type: DirectoryOrCreate + name: clickhouse-port-config diff --git a/router/config.yaml b/router/config.yaml index 80b42939..a30ed2ad 100644 --- a/router/config.yaml +++ b/router/config.yaml @@ -34,26 +34,26 @@ classification_rules: # The api_key placeholder is resolved at runtime by main.py:788. backends: - name: "agent-simple-core" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" - name: "agent-medium-core" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" - name: "agent-complex-core" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" - name: "agent-reasoning-core" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" - name: "agent-advanced-core" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" - name: "ollama-deepseek-v4-pro" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" - name: "ollama-deepseek-v4-flash" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" - name: "openrouter-auto" - api_base: "http://127.0.0.1:4000/v1" + api_base: "http://127.0.0.1:LITELLM_PORT_PLACEHOLDER/v1" api_key: "DYNAMIC_LITELLM_MASTER_KEY_PLACEHOLDER" diff --git a/router/main.py b/router/main.py index 7b8df8d9..6ca2e7c3 100644 --- a/router/main.py +++ b/router/main.py @@ -23,9 +23,9 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator, RootModel from typing import Dict, Optional, Union -LITELLM_URL = (os.getenv("LITELLM_ADMIN_URL") or "http://127.0.0.1:4000").rstrip("/") +LITELLM_URL = (os.getenv("LITELLM_ADMIN_URL") or f"http://127.0.0.1:{os.getenv('LITELLM_PORT', '4000')}").rstrip("/") LLAMA_SERVER_URL = (os.getenv("LLAMA_SERVER_URL") or "http://127.0.0.1:8080").rstrip("/") -LANGFUSE_HOST = (os.getenv("LANGFUSE_HOST") or "http://127.0.0.1:3001").rstrip("/") +LANGFUSE_HOST = (os.getenv("LANGFUSE_HOST") or f"http://127.0.0.1:{os.getenv('LANGFUSE_WEB_PORT', '3001')}").rstrip("/") GEMINI_OAUTH_CREDS_PATH = "/config/gemini_auth/oauth_creds.json" @@ -41,6 +41,10 @@ _redis_last_init_attempt = 0.0 _REDIS_RETRY_INTERVAL_SECONDS = 5.0 +def _valkey_port() -> str: + """Resolve the Valkey cache port from env, preferring VALKEY_CACHE_PORT.""" + return os.getenv("VALKEY_CACHE_PORT") or os.getenv("VALKEY_PORT", "6379") + def get_redis(): """Lazily initialize and return the async Redis/Valkey client. Returns None if connection fails or is disabled (non-fatal fallback).""" @@ -57,7 +61,7 @@ def get_redis(): logger.info("Valkey client initialized from URL") else: host = os.getenv("VALKEY_HOST", "127.0.0.1") - port = int(os.getenv("VALKEY_PORT", "6379")) + port = int(_valkey_port()) _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: @@ -1590,7 +1594,7 @@ def get_pie_chart_gradient() -> str: @app.api_route("/v1/memory{path:path}", methods=["GET", "POST", "DELETE", "PUT"]) async def proxy_memory(request: Request, path: str = ""): """Proxies memory API calls to the LiteLLM gateway on port 4000.""" - litellm_base = "http://127.0.0.1:4000/v1/memory" + litellm_base = f"http://127.0.0.1:{os.getenv('LITELLM_PORT', '4000')}/v1/memory" # Resolve the destination URL url = f"{litellm_base}{path}" @@ -2666,10 +2670,10 @@ async def get_dashboard_data(): llamacpp, ) = await asyncio.gather( asyncio.wait_for(sync_cooldowns_from_valkey(), timeout=2.0), - check_tcp_port("127.0.0.1", 6379), - check_http_endpoint("http://127.0.0.1:4000/"), + check_tcp_port("127.0.0.1", int(_valkey_port())), + check_http_endpoint(f"http://127.0.0.1:{os.getenv('LITELLM_PORT', '4000')}/"), check_http_endpoint("http://127.0.0.1:8080/health"), - check_http_endpoint("http://127.0.0.1:3001"), + check_http_endpoint(f"http://127.0.0.1:{os.getenv('LANGFUSE_WEB_PORT', '3001')}"), get_gemini_oauth_status(), asyncio.wait_for(get_best_free_model(), timeout=5.0), asyncio.to_thread(get_goose_sessions), @@ -3867,7 +3871,7 @@ async def get_dashboard(request: Request):
Triage Router - :5000 + :{os.getenv('ROUTER_PORT', '5000')}
Online
@@ -3875,7 +3879,7 @@ async def get_dashboard(request: Request):
LiteLLM Proxy - :4000 + :{os.getenv('LITELLM_PORT', '4000')}
{"Online" if litellm_status else "Offline"} @@ -3885,7 +3889,7 @@ async def get_dashboard(request: Request):
Valkey Cache - :6379 + :{_valkey_port()}
{"Online" if valkey_status else "Offline"} @@ -3905,7 +3909,7 @@ async def get_dashboard(request: Request):
Langfuse Traces - :3001 + :{os.getenv('LANGFUSE_WEB_PORT', '3001')}
{"Online" if langfuse_status else "Offline"} diff --git a/scripts/backup.sh b/scripts/backup.sh index 2000ddb7..4a4159e1 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -13,6 +13,12 @@ BACKUP_DIR="${WORKDIR}/backups" RETENTION_DAYS=14 LOG_FILE="/tmp/llm-backup-${TIMESTAMP}.log" +# Source .env for POD_NAME (with prod default) +if [ -f "${WORKDIR}/.env" ]; then + set -a; source "${WORKDIR}/.env"; set +a +fi +POD_NAME="${POD_NAME:-agent-router-pod}" + mkdir -p "$BACKUP_DIR" log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG_FILE"; } @@ -21,12 +27,12 @@ log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG_FILE"; } log "โณ Checking PostgreSQL readiness..." PG_READY=0 # Check if container exists AND is running before looping -PG_RUNNING=$(podman inspect --format '{{.State.Running}}' agent-router-pod-postgres-db 2>/dev/null || echo "false") +PG_RUNNING=$(podman inspect --format '{{.State.Running}}' ${POD_NAME}-postgres-db 2>/dev/null || echo "false") if [ "$PG_RUNNING" != "true" ]; then log "โš ๏ธ PostgreSQL container not running โ€” skipping DB backup" else for i in {1..15}; do - if podman exec agent-router-pod-postgres-db pg_isready -U postgres 2>/dev/null; then + if podman exec ${POD_NAME}-postgres-db pg_isready -U postgres 2>/dev/null; then PG_READY=1 log "โœ… PostgreSQL is ready" break @@ -40,7 +46,7 @@ fi if [ $PG_READY -eq 1 ]; then for db in postgres langfuse; do FILE="${BACKUP_DIR}/${db}_db_${TIMESTAMP}.dump" - if podman exec agent-router-pod-postgres-db \ + if podman exec ${POD_NAME}-postgres-db \ pg_dump -U postgres -d "$db" -F c > "$FILE"; then log "โœ… ${db} db: $(ls -lh "$FILE" | awk '{print $5}')" else diff --git a/start-dev.sh b/start-dev.sh new file mode 100755 index 00000000..602d9746 --- /dev/null +++ b/start-dev.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Dev deployment wrapper โ€” stands up dev-router-pod on distinct ports alongside prod +set -e +WORKDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$WORKDIR" + +export DEV_ENV_FILE="${WORKDIR}/.env.dev" + +exec bash start-stack.sh "$@" diff --git a/start-stack.sh b/start-stack.sh index 4f0f9588..be86e4a5 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -16,7 +16,8 @@ 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 --pull โ†’ Pull latest router image from GHCR and recreate/redeploy pod" + echo " ./start-stack.sh --full-rebuild โ†’ Rebuild custom router image locally and recreate/redeploy pod" echo " ./start-stack.sh --help | -h โ†’ Show this help message and exit" } @@ -36,12 +37,12 @@ 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 - +PULL_MODE=false FULL_REBUILD=false REPLACE_MODE=false -if [ "${1:-}" = "--full-rebuild" ]; then +if [ "${1:-}" = "--pull" ] || [ "${1:-}" = "--pull-latest" ]; then + PULL_MODE=true +elif [ "${1:-}" = "--full-rebuild" ]; then FULL_REBUILD=true elif [ "${1:-}" = "--replace" ]; then REPLACE_MODE=true @@ -69,6 +70,34 @@ if [ -f "$ENV_FILE" ]; then set +a fi +# Load optional dev-environment overlay (set DEV_ENV_FILE before calling this script) +if [ -n "${DEV_ENV_FILE:-}" ] && [ -f "$DEV_ENV_FILE" ]; then + set -a + source "$DEV_ENV_FILE" + set +a +fi + +# Port assignments โ€” read from env (set by .env or .env.dev) with prod defaults +POD_NAME="${POD_NAME:-agent-router-pod}" +ROUTER_PORT="${ROUTER_PORT:-5000}" +LITELLM_PORT="${LITELLM_PORT:-4000}" +LANGFUSE_WEB_PORT="${LANGFUSE_WEB_PORT:-3001}" +LANGFUSE_WORKER_PORT="${LANGFUSE_WORKER_PORT:-3030}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +VALKEY_CACHE_PORT="${VALKEY_CACHE_PORT:-6379}" +VALKEY_LF_PORT="${VALKEY_LF_PORT:-6380}" +CLICKHOUSE_HTTP_PORT="${CLICKHOUSE_HTTP_PORT:-8123}" +CLICKHOUSE_TCP_PORT="${CLICKHOUSE_TCP_PORT:-9000}" +CLICKHOUSE_INTERSERVER_PORT="${CLICKHOUSE_INTERSERVER_PORT:-9009}" +MINIO_S3_PORT="${MINIO_S3_PORT:-9002}" +MINIO_CONSOLE_PORT="${MINIO_CONSOLE_PORT:-9001}" +ROUTER_IMAGE="${ROUTER_IMAGE:-ghcr.io/sheepdestroyer/llm-routing:latest}" +DATA_ROOT="${DATA_ROOT:-${WORKDIR}/data}" +export POD_NAME ROUTER_PORT LITELLM_PORT LANGFUSE_WEB_PORT LANGFUSE_WORKER_PORT POSTGRES_PORT VALKEY_CACHE_PORT VALKEY_LF_PORT CLICKHOUSE_HTTP_PORT CLICKHOUSE_TCP_PORT CLICKHOUSE_INTERSERVER_PORT MINIO_S3_PORT MINIO_CONSOLE_PORT ROUTER_IMAGE DATA_ROOT + +# Ensure local volume directories exist on the host for Podman mounts +mkdir -p "${DATA_ROOT}/valkey-data" "${DATA_ROOT}/postgres-data" "${DATA_ROOT}/langfuse-data" "${DATA_ROOT}/clickhouse-data" "${DATA_ROOT}/redis-lf-data" "${DATA_ROOT}/minio-data" + # Define and export the routing domain ROUTING_DOMAIN="${ROUTING_DOMAIN:-vendeuvre.lan}" export ROUTING_DOMAIN @@ -82,12 +111,9 @@ if [[ ! "$PUBLIC_BASE_URL" =~ /llm-routing ]]; then PUBLIC_BASE_URL="${PUBLIC_BASE_URL%/}/llm-routing" fi PUBLIC_BASE_URL="${PUBLIC_BASE_URL%/}" -LOCAL_BASE_URL="${LOCAL_BASE_URL:-http://localhost:5000}" +LOCAL_BASE_URL="${LOCAL_BASE_URL:-http://localhost:${ROUTER_PORT}}" LOCAL_BASE_URL="${LOCAL_BASE_URL%/}" -LITELLM_PUBLIC_URL="${LITELLM_PUBLIC_URL:-https://litellm.x570.${ROUTING_DOMAIN}}" -LANGFUSE_PUBLIC_URL="${LANGFUSE_PUBLIC_URL:-https://langfuse.x570.${ROUTING_DOMAIN}}" -LITELLM_ROOT_PATH="${LITELLM_ROOT_PATH:-}" -export PUBLIC_BASE_URL LOCAL_BASE_URL LITELLM_PUBLIC_URL LANGFUSE_PUBLIC_URL LITELLM_ROOT_PATH +export PUBLIC_BASE_URL LOCAL_BASE_URL # Ensure openssl is installed if we need to generate passwords/keys @@ -337,7 +363,7 @@ fi # Hermes profiles (e.g., llm-routing-openrouter) whose container storage # can leave surviving processes holding ports indefinitely. cleanup_zombie_ports() { - local ALL_PORTS="3000 3030 4000 5000 5005 5432 6379 6380 8080 8123 9000 9001 9002 9004 9005 9009" + local ALL_PORTS="$ROUTER_PORT $LITELLM_PORT $LANGFUSE_WEB_PORT $LANGFUSE_WORKER_PORT $POSTGRES_PORT $VALKEY_CACHE_PORT $VALKEY_LF_PORT $CLICKHOUSE_HTTP_PORT $CLICKHOUSE_TCP_PORT $CLICKHOUSE_INTERSERVER_PORT $MINIO_S3_PORT $MINIO_CONSOLE_PORT 8080 9004 9005" echo "๐Ÿงน Cleaning up zombie port bindings..." @@ -411,9 +437,9 @@ setup_minio_buckets() { echo "" echo "๐Ÿ“ฆ Ensuring MinIO buckets exist..." - # Wait for MinIO S3 API to be ready (port 9002) + # Wait for MinIO S3 API to be ready while [ $waited -lt $MAX_WAIT ]; do - if curl -sf --max-time 3 http://127.0.0.1:9002/minio/health/live >/dev/null 2>&1; then + if curl -sf --max-time 3 http://127.0.0.1:${MINIO_S3_PORT}/minio/health/live >/dev/null 2>&1; then echo " โœ“ MinIO S3 API ready after ${waited}s" break fi @@ -425,22 +451,22 @@ setup_minio_buckets() { return 1 fi - # Ensure mc alias points to the correct MinIO S3 API port (9002, not 9000) + # Ensure mc alias points to the correct MinIO S3 API port # The default 'local' alias in the MinIO image points to :9000 which is ClickHouse, # not MinIO. We must override it. - 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 + if ! podman exec ${POD_NAME}-minio-s3 mc alias set local http://127.0.0.1:${MINIO_S3_PORT} "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; then + echo "โŒ Error: Failed to set MinIO alias 'local' on http://127.0.0.1:${MINIO_S3_PORT}" >&2 exit 1 fi # Create required buckets (idempotent) local BUCKETS=("langfuse-events" "proj-triage-gateway-id") for bucket in "${BUCKETS[@]}"; do - if podman exec agent-router-pod-minio-s3 mc ls "local/${bucket}" >/dev/null 2>&1; then + if podman exec ${POD_NAME}-minio-s3 mc ls "local/${bucket}" >/dev/null 2>&1; then echo " โœ“ Bucket '${bucket}' exists" else echo " + Creating bucket '${bucket}'..." - podman exec agent-router-pod-minio-s3 mc mb "local/${bucket}" 2>/dev/null || { + podman exec ${POD_NAME}-minio-s3 mc mb "local/${bucket}" 2>/dev/null || { echo " โš ๏ธ Failed to create bucket '${bucket}'" } fi @@ -459,7 +485,7 @@ verify_stack_health() { # Wait for postgres first โ€” everything depends on it while [ $waited -lt $MAX_WAIT ]; do - if podman exec agent-router-pod-postgres-db pg_isready -U postgres -q 2>/dev/null; then + if podman exec ${POD_NAME}-postgres-db pg_isready -U postgres -p ${POSTGRES_PORT} -q 2>/dev/null; then echo " โœ“ PostgreSQL ready after ${waited}s" break fi @@ -475,7 +501,7 @@ verify_stack_health() { local litellm_ready=false waited=0 while [ $waited -lt $MAX_WAIT ]; do - if curl -sf --max-time 3 http://127.0.0.1:4000/health/readiness >/dev/null 2>&1; then + if curl -sf --max-time 3 http://127.0.0.1:${LITELLM_PORT}/health/readiness >/dev/null 2>&1; then echo " โœ“ LiteLLM ready after ${waited}s" litellm_ready=true break @@ -490,7 +516,7 @@ verify_stack_health() { # Wait for triage router + verify full pipeline waited=0 while [ $waited -lt 120 ]; do - local resp=$(curl -s --max-time 10 http://127.0.0.1:5000/v1/chat/completions \ + local resp=$(curl -s --max-time 10 http://127.0.0.1:${ROUTER_PORT}/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{"model":"agent-simple-core","messages":[{"role":"user","content":"Hi"}],"max_tokens":5}' 2>/dev/null) if echo "$resp" | grep -q '"choices"'; then @@ -508,16 +534,16 @@ verify_stack_health() { # Graceful stop (SIGTERM with 30s timeout) lets ClickHouse/Postgres flush, # then force-remove if needed. Avoids data corruption from SIGKILL. safe_pod_teardown() { - if podman pod exists agent-router-pod 2>/dev/null; then + if podman pod exists ${POD_NAME} 2>/dev/null; then echo "๐Ÿ›‘ Gracefully stopping pod (SIGTERM, 30s timeout)..." - podman pod stop -t 30 agent-router-pod 2>/dev/null || true + podman pod stop -t 30 ${POD_NAME} 2>/dev/null || true # If still running after graceful attempt, force-remove - if podman pod exists agent-router-pod 2>/dev/null; then + if podman pod exists ${POD_NAME} 2>/dev/null; then echo "โš ๏ธ Graceful stop timed out โ€” force-removing..." - podman pod rm -f agent-router-pod 2>/dev/null || true + podman pod rm -f ${POD_NAME} 2>/dev/null || true else # Already stopped, just remove - podman pod rm agent-router-pod 2>/dev/null || true + podman pod rm ${POD_NAME} 2>/dev/null || true fi cleanup_zombie_ports echo "โœ“ Pod torn down, ports cleaned" @@ -526,13 +552,62 @@ safe_pod_teardown() { # Pre-deploy database backup (runs before any pod modification) # Skip if pod doesn't exist (e.g., after manual cleanup) -if podman pod exists agent-router-pod 2>/dev/null; then +if podman pod exists ${POD_NAME} 2>/dev/null; then echo "๐Ÿ’พ Taking pre-deploy database backup..." bash scripts/backup.sh && echo "โœ“ Pre-deploy backup saved" || echo "โš ๏ธ Pre-deploy backup skipped" fi +# โ”€โ”€ ClickHouse port override XML โ”€โ”€ +# Writes a minimal config.d XML override so ClickHouse listens on the +# configured ports instead of its compiled-in defaults. +generate_clickhouse_config() { + local config_dir="${DATA_ROOT}/clickhouse-config" + mkdir -p "$config_dir" + cat > "${config_dir}/port-override.xml" << EOF + + ${CLICKHOUSE_HTTP_PORT} + ${CLICKHOUSE_TCP_PORT} + ${CLICKHOUSE_INTERSERVER_PORT} + +EOF + echo "โœ“ ClickHouse port config written to ${config_dir}/port-override.xml" +} + +# โ”€โ”€ LiteLLM rendered config โ”€โ”€ +# Generates a rendered config.yaml (with port substitutions) into DATA_ROOT/litellm-rendered/ +# so prod and dev each get their own copy with the correct port values. +render_litellm_config() { + local rendered_dir="${DATA_ROOT}/litellm-rendered" + mkdir -p "$rendered_dir" + sed -e "s/VALKEY_CACHE_PORT_PLACEHOLDER/${VALKEY_CACHE_PORT}/g" \ + -e "s/ROUTER_PORT_PLACEHOLDER/${ROUTER_PORT}/g" \ + "${WORKDIR}/litellm/config.yaml" > "${rendered_dir}/config.yaml" + # Validate no unresolved placeholders remain + if grep -q 'VALKEY_CACHE_PORT_PLACEHOLDER\|ROUTER_PORT_PLACEHOLDER' "${rendered_dir}/config.yaml"; then + echo "โŒ Error: Unresolved placeholders remain in ${rendered_dir}/config.yaml" >&2 + exit 1 + fi + chmod 644 "${rendered_dir}/config.yaml" + # Copy entrypoint.py unchanged + cp "${WORKDIR}/litellm/entrypoint.py" "${rendered_dir}/entrypoint.py" + chmod 644 "${rendered_dir}/entrypoint.py" + echo "โœ“ LiteLLM config rendered to ${rendered_dir}/config.yaml" +} + +# โ”€โ”€ Router rendered config โ”€โ”€ +# Generates a rendered config.yaml (with port substitutions) into DATA_ROOT/router-rendered/ +# so prod and dev each get their own copy with the correct LiteLLM port. +render_router_config() { + local rendered_dir="${DATA_ROOT}/router-rendered" + mkdir -p "$rendered_dir" + sed -e "s/LITELLM_PORT_PLACEHOLDER/${LITELLM_PORT}/g" \ + "${WORKDIR}/router/config.yaml" > "${rendered_dir}/config.yaml" + chmod 644 "${rendered_dir}/config.yaml" + echo "โœ“ Router config rendered to ${rendered_dir}/config.yaml" +} + 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 PUBLIC_BASE_URL ROUTING_DOMAIN LITELLM_PUBLIC_URL LANGFUSE_PUBLIC_URL LITELLM_ROOT_PATH + 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 PUBLIC_BASE_URL ROUTING_DOMAIN POD_NAME DATA_ROOT ROUTER_IMAGE ROUTER_PORT LITELLM_PORT LANGFUSE_WEB_PORT LANGFUSE_WORKER_PORT POSTGRES_PORT VALKEY_CACHE_PORT VALKEY_LF_PORT CLICKHOUSE_HTTP_PORT CLICKHOUSE_TCP_PORT CLICKHOUSE_INTERSERVER_PORT MINIO_S3_PORT MINIO_CONSOLE_PORT python3 - "$WORKDIR/pod.yaml" <<'PY' import os, sys, urllib.parse, json uid = os.getuid() @@ -563,8 +638,20 @@ placeholders = [ "PROXY_BASE_URL_PLACEHOLDER", "PUBLIC_BASE_URL_PLACEHOLDER", "ROUTING_DOMAIN_PLACEHOLDER", - "NEXTAUTH_URL_PLACEHOLDER", - "SERVER_ROOT_PATH_PLACEHOLDER" + "POD_NAME_PLACEHOLDER", + "DATA_ROOT_PLACEHOLDER", + "ROUTER_PORT_PLACEHOLDER", + "LITELLM_PORT_PLACEHOLDER", + "LANGFUSE_WEB_PORT_PLACEHOLDER", + "LANGFUSE_WORKER_PORT_PLACEHOLDER", + "POSTGRES_PORT_PLACEHOLDER", + "VALKEY_CACHE_PORT_PLACEHOLDER", + "VALKEY_LF_PORT_PLACEHOLDER", + "CLICKHOUSE_HTTP_PORT_PLACEHOLDER", + "CLICKHOUSE_TCP_PORT_PLACEHOLDER", + "MINIO_S3_PORT_PLACEHOLDER", + "MINIO_CONSOLE_PORT_PLACEHOLDER", + "ROUTER_IMAGE_PLACEHOLDER", ] for ph in placeholders: if ph not in text: @@ -589,11 +676,31 @@ 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"])) -text = text.replace("PROXY_BASE_URL_PLACEHOLDER", yaml_scalar(os.environ["LITELLM_PUBLIC_URL"])) -text = text.replace("NEXTAUTH_URL_PLACEHOLDER", yaml_scalar(os.environ["LANGFUSE_PUBLIC_URL"])) -text = text.replace("SERVER_ROOT_PATH_PLACEHOLDER", yaml_scalar(os.environ["LITELLM_ROOT_PATH"])) +# Derive PROXY_BASE_URL from PUBLIC_BASE_URL +public_base_url = os.environ["PUBLIC_BASE_URL"].rstrip("/") +proxy_base_url = f"{public_base_url}/litellm" +text = text.replace("PROXY_BASE_URL_PLACEHOLDER", yaml_scalar(proxy_base_url)) text = text.replace("PUBLIC_BASE_URL_PLACEHOLDER", yaml_scalar(os.environ["PUBLIC_BASE_URL"])) text = text.replace("ROUTING_DOMAIN_PLACEHOLDER", yaml_scalar(os.environ["ROUTING_DOMAIN"])) +# Raw replacements (no quoting โ€” used for pod name, data root, and integer port values) +raw_replacements = { + "POD_NAME_PLACEHOLDER": os.environ["POD_NAME"], + "DATA_ROOT_PLACEHOLDER": os.environ["DATA_ROOT"], + "ROUTER_PORT_PLACEHOLDER": os.environ["ROUTER_PORT"], + "LITELLM_PORT_PLACEHOLDER": os.environ["LITELLM_PORT"], + "LANGFUSE_WEB_PORT_PLACEHOLDER": os.environ["LANGFUSE_WEB_PORT"], + "LANGFUSE_WORKER_PORT_PLACEHOLDER": os.environ["LANGFUSE_WORKER_PORT"], + "POSTGRES_PORT_PLACEHOLDER": os.environ["POSTGRES_PORT"], + "VALKEY_CACHE_PORT_PLACEHOLDER": os.environ["VALKEY_CACHE_PORT"], + "VALKEY_LF_PORT_PLACEHOLDER": os.environ["VALKEY_LF_PORT"], + "CLICKHOUSE_HTTP_PORT_PLACEHOLDER": os.environ["CLICKHOUSE_HTTP_PORT"], + "CLICKHOUSE_TCP_PORT_PLACEHOLDER": os.environ["CLICKHOUSE_TCP_PORT"], + "MINIO_S3_PORT_PLACEHOLDER": os.environ["MINIO_S3_PORT"], + "MINIO_CONSOLE_PORT_PLACEHOLDER": os.environ["MINIO_CONSOLE_PORT"], + "ROUTER_IMAGE_PLACEHOLDER": os.environ["ROUTER_IMAGE"], +} +for ph, val in raw_replacements.items(): + text = text.replace(ph, val) import re unresolved = sorted(set(re.findall(r"\b[A-Z0-9_]+_PLACEHOLDER\b", text))) if unresolved: @@ -607,24 +714,41 @@ sys.stdout.write(text) PY } -if podman pod exists agent-router-pod 2>/dev/null; then +if podman pod exists ${POD_NAME} 2>/dev/null; then if $FULL_REBUILD; then echo "๐Ÿ”จ Building custom local triage router image..." - podman build -t localhost/llm-triage-router:latest -f router/Dockerfile router + podman build -t "${ROUTER_IMAGE}" -f router/Dockerfile router safe_pod_teardown echo "๐Ÿš€ Deploying fresh triage pod..." + generate_clickhouse_config + render_litellm_config + render_router_config + render_pod_yaml | podman play kube - + setup_minio_buckets + verify_stack_health + elif $PULL_MODE; then + echo "๐Ÿšš Pulling latest triage router image from GHCR..." + podman pull "${ROUTER_IMAGE}" + safe_pod_teardown + echo "๐Ÿš€ Deploying fresh triage pod with pulled image..." + generate_clickhouse_config + render_litellm_config + render_router_config render_pod_yaml | podman play kube - setup_minio_buckets verify_stack_health elif $REPLACE_MODE; then safe_pod_teardown echo "๐Ÿš€ Deploying replacement pod from YAML..." + generate_clickhouse_config + render_litellm_config + render_router_config render_pod_yaml | podman play kube - setup_minio_buckets verify_stack_health else - echo "๐Ÿ”„ Restarting existing agent-router-pod (use --replace or --full-rebuild to recreate)..." - podman pod restart agent-router-pod + echo "๐Ÿ”„ Restarting existing ${POD_NAME} (use --replace or --pull to recreate)..." + podman pod restart ${POD_NAME} setup_minio_buckets verify_stack_health @@ -643,10 +767,18 @@ if podman pod exists agent-router-pod 2>/dev/null; then else # First deploy โ€” no pod exists, clean ports just in case cleanup_zombie_ports - echo "๐Ÿ”จ Building custom local triage router image..." - podman build -t localhost/llm-triage-router:latest -f router/Dockerfile router + if $FULL_REBUILD; then + echo "๐Ÿ”จ Building custom local triage router image..." + podman build -t "${ROUTER_IMAGE}" -f router/Dockerfile router + else + echo "๐Ÿšš Pulling latest triage router image from GHCR..." + podman pull "${ROUTER_IMAGE}" + fi echo "๐Ÿš€ No existing pod found. Deploying fresh triage pod..." + generate_clickhouse_config + render_litellm_config + render_router_config render_pod_yaml | podman play kube - setup_minio_buckets verify_stack_health