feat(agent): hierarchical executor with path resolve, runtime probe, quiet UI
CI / build-gateway (push) Failing after 16s
CI / sync-config (push) Failing after 0s

Make Zed Agent closer to Cursor: deterministic DevOps path index, live Traefik
port probe before blind edits, stop-after-edit, and quieter Russian progress.
This commit is contained in:
2026-08-13 11:41:27 +03:00
parent 6fd2f0e689
commit a2d238d92e
71 changed files with 10617 additions and 449 deletions
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
echo "== Tokens/users now =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT count(*) AS tokens FROM \"LiteLLM_VerificationToken\";
SELECT count(*) AS users FROM \"LiteLLM_UserTable\";
SELECT \"user_id\", \"user_role\" FROM \"LiteLLM_UserTable\" LIMIT 5;
"
echo "== Master key models probe =="
code=$(curl -sS -o /tmp/m.json -w "%{http_code}" --max-time 20 \
-H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
https://litellm.ift.calentiq.com/v1/models)
echo "models HTTP $code"
head -c 120 /tmp/m.json; echo
echo "== Flush redis key cache (best effort) =="
RID=$(docker ps -q -f name=ai-router_redis | head -1)
if [[ -n "$RID" ]]; then
docker exec "$RID" redis-cli KEYS '*token*' 2>/dev/null | head -20 || true
docker exec "$RID" redis-cli KEYS '*litellm*' 2>/dev/null | head -20 || true
# do not FLUSHALL — may kill router session cache; only clear litellm-ish if safe
fi
echo DONE
REMOTE
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
echo "== curl observer =="
curl -sS -o /tmp/obs.body -w "http=%{http_code} time=%{time_total}\n" \
--max-time 15 -k https://observer.ift.calentiq.com/ || true
head -c 400 /tmp/obs.body; echo
echo "== swarm observer_web =="
docker service ls 2>/dev/null | grep -i observer || true
docker service ps $(docker service ls -q --filter name=observer 2>/dev/null | head -1) --no-trunc 2>/dev/null | head -8 || \
docker ps -a --filter name=observer --format '{{.Names}} {{.Status}}' | head -10
echo "== network aliases =="
docker network inspect eventhub-ift-net --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}' 2>/dev/null | grep -i observer || true
REMOTE
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Explain + delete spurious LiteLLM failure logs from restart disconnects
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
USER="${POSTGRES_USER:-litellm}"
DB="${POSTGRES_DB:-litellm}"
echo "== Error message sample =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "$USER" -d "$DB" -t -A -c "
SELECT left(metadata->'error_information'->>'error_message', 200)
FROM \"LiteLLM_SpendLogs\"
WHERE status='failure'
LIMIT 3;
"
echo "== Delete empty-model restart failures =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "$USER" -d "$DB" -c "
DELETE FROM \"LiteLLM_SpendLogs\"
WHERE status = 'failure'
AND (model IS NULL OR model = '')
AND (call_type IS NULL OR call_type = '');
"
echo "== Remaining =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "$USER" -d "$DB" -c "
SELECT status, count(*) FROM \"LiteLLM_SpendLogs\" GROUP BY 1;
"
echo CLEANED_SPURIOUS_FAILS_OK
REMOTE
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Clear LiteLLM Postgres spend/logs on IFT (keep schema + keys if present)
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
if [[ -z "$PG" ]]; then
echo "ERROR: postgres container not running" >&2
exit 1
fi
USER="${POSTGRES_USER:-litellm}"
DB="${POSTGRES_DB:-litellm}"
echo "== Tables before =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "$USER" -d "$DB" -c "\dt"
# LiteLLM Prisma tables — truncate data, keep schema
# Prefer spend/usage; also clear invite/audit-ish if present
SQL=$(cat <<'EOS'
DO $$
DECLARE
r RECORD;
n bigint;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename NOT IN ('_prisma_migrations')
LOOP
EXECUTE format('SELECT count(*) FROM %I', r.tablename) INTO n;
RAISE NOTICE 'truncate % (% rows)', r.tablename, n;
EXECUTE format('TRUNCATE TABLE %I RESTART IDENTITY CASCADE', r.tablename);
END LOOP;
END $$;
EOS
)
echo "== Truncate all public tables (except _prisma_migrations) =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" -i "$PG" \
psql -U "$USER" -d "$DB" -v ON_ERROR_STOP=1 <<< "$SQL"
echo "== Counts after =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "$USER" -d "$DB" -c "
SELECT relname AS table, n_live_tup AS approx_rows
FROM pg_stat_user_tables
ORDER BY relname;
"
echo "== Restart litellm (refresh Admin UI caches) =="
docker service update --force ai-router_litellm >/dev/null
for i in $(seq 1 40); do
curl -sf --max-time 5 https://litellm.ift.calentiq.com/health/liveliness >/dev/null && break
sleep 2
done
curl -sf --max-time 5 https://litellm.ift.calentiq.com/health/liveliness && echo
echo LITELLM_DB_CLEARED_OK
REMOTE
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Deploy Cursor-parity AiRouter (Max plan / Coder routine / DeepSeek hard) to IFT
set -euo pipefail
AIR=/mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubAiRouter
cd "$AIR"
echo "== Local unit + compile =="
python3 -m py_compile \
router/agent_hier.py router/router.py router/hierarchical.py router/orchestrator.py
python3 -m unittest discover -s test/unit -q
PRIMARY_PROVIDER=hybrid python3 scripts/gen-litellm-config.py
echo "== Sync to eventhub-ift =="
scp \
router/agent_hier.py \
router/router.py \
router/hierarchical.py \
router/orchestrator.py \
eventhub-ift:/opt/ai-router-stack/router/
scp \
config/orchestration.yaml \
config/providers.yaml \
config/routing_rules.yaml \
eventhub-ift:/opt/ai-router-stack/config/
scp litellm_config.yaml eventhub-ift:/opt/ai-router-stack/litellm_config.yaml
scp README.md AGENTS.md eventhub-ift:/opt/ai-router-stack/ 2>/dev/null || true
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
export PRIMARY_PROVIDER="${PRIMARY_PROVIDER:-hybrid}"
sed -i 's/\r$//' \
router/agent_hier.py router/router.py router/hierarchical.py router/orchestrator.py \
config/orchestration.yaml config/providers.yaml config/routing_rules.yaml \
litellm_config.yaml || true
echo "== Build gateway image =="
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
TS=$(date +%Y%m%d%H%M%S)
ORCH="orchestration_config_${TS}"
LITC="litellm_config_${TS}"
docker config create "$ORCH" ./config/orchestration.yaml
docker config create "$LITC" ./litellm_config.yaml
OLD_ORCH=$(docker service inspect ai-router_router --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep orchestration | tail -1 || true)
OLD_LIT=$(docker service inspect ai-router_litellm --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep litellm_config | grep -v entrypoint | tail -1 || true)
echo "== Update ai-router_router =="
RARGS=(--image "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" --force)
RARGS+=(--config-add "source=${ORCH},target=/app/config/orchestration.yaml")
if [ -n "${OLD_ORCH:-}" ]; then
RARGS+=(--config-rm "$OLD_ORCH")
fi
docker service update "${RARGS[@]}" ai-router_router >/dev/null
echo "== Update ai-router_litellm =="
LARGS=(--force)
LARGS+=(--config-add "source=${LITC},target=/app/config.yaml")
if [ -n "${OLD_LIT:-}" ]; then
LARGS+=(--config-rm "$OLD_LIT")
fi
docker service update "${LARGS[@]}" ai-router_litellm >/dev/null
echo "== Wait health =="
ok=0
for i in $(seq 1 60); do
if curl -sf --max-time 5 https://ai-router.ift.calentiq.com/health >/dev/null \
&& curl -sf --max-time 5 https://litellm.ift.calentiq.com/health/liveliness >/dev/null; then
ok=1
break
fi
sleep 3
done
if [ "$ok" != 1 ]; then
echo "ERROR: health not ready" >&2
docker service ps ai-router_router --no-trunc | head -8
docker service ps ai-router_litellm --no-trunc | head -8
exit 1
fi
RID=$(docker ps -q -f name=ai-router_router | head -1)
LID=$(docker ps -q -f name=ai-router_litellm | head -1)
echo "== Verify orchestration =="
docker exec "$RID" grep -E 'planner_model:|verifier_model:|agent_executor_model:|executor_synthetic_mode:|worker_map:|hard:' /app/config/orchestration.yaml
echo "== Verify litellm novita-planner =="
docker exec "$LID" sh -c 'grep -A6 "model_name: novita-planner" /app/config.yaml | head -8'
echo "== Smoke classify + chat =="
curl -sf --max-time 30 -H "Authorization: Bearer ${ROUTER_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"text":"поправь timeout в router.py"}' \
https://ai-router.ift.calentiq.com/classify | head -c 400
echo
curl -sf --max-time 90 -H "Authorization: Bearer ${ROUTER_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"model":"smart-router","messages":[{"role":"user","content":"ping"}],"max_tokens":8}' \
https://ai-router.ift.calentiq.com/v1/chat/completions | head -c 500
echo
echo CURSOR_PARITY_DEPLOY_OK
REMOTE
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
PYTHONPATH=router python3 -m py_compile router/agent_hier.py router/router.py
PYTHONPATH=router python3 -m unittest discover -s test/unit -q
scp router/agent_hier.py router/router.py \
eventhub-ift:/opt/ai-router-stack/router/
scp config/orchestration.yaml eventhub-ift:/opt/ai-router-stack/config/
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
sed -i 's/\r$//' router/agent_hier.py router/router.py config/orchestration.yaml || true
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
TS=$(date +%Y%m%d%H%M%S)
ORCH="orchestration_config_${TS}"
docker config create "$ORCH" ./config/orchestration.yaml
OLD_ORCH=$(docker service inspect ai-router_router --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep orchestration | tail -1 || true)
RARGS=(--image "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" --force)
RARGS+=(--config-add "source=${ORCH},target=/app/config/orchestration.yaml")
[[ -n "${OLD_ORCH:-}" ]] && RARGS+=(--config-rm "$OLD_ORCH")
docker service update "${RARGS[@]}" ai-router_router >/dev/null
for i in $(seq 1 40); do
curl -sf --max-time 5 https://ai-router.ift.calentiq.com/health >/dev/null && break
sleep 2
done
docker exec "$(docker ps -q -f name=ai-router_router | head -1)" \
grep -E 'executor_tool_loop_timeout_sec|executor_escalate_after_read|executor_rewrite_reread|executor_newest_tool' \
/app/config/orchestration.yaml
echo FAST_AFTER_READ_OK
REMOTE
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
AIR=/mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubAiRouter
cd "$AIR"
python3 -m py_compile router/agent_hier.py router/agent_stream.py router/router.py
python3 -m unittest discover -s test/unit -q
scp router/agent_hier.py router/agent_stream.py router/router.py \
eventhub-ift:/opt/ai-router-stack/router/
scp config/orchestration.yaml eventhub-ift:/opt/ai-router-stack/config/
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
sed -i 's/\r$//' router/agent_hier.py router/agent_stream.py router/router.py config/orchestration.yaml || true
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
TS=$(date +%Y%m%d%H%M%S)
ORCH="orchestration_config_${TS}"
docker config create "$ORCH" ./config/orchestration.yaml
OLD_ORCH=$(docker service inspect ai-router_router --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep orchestration | tail -1 || true)
RARGS=(--image "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" --force)
RARGS+=(--config-add "source=${ORCH},target=/app/config/orchestration.yaml")
[[ -n "${OLD_ORCH:-}" ]] && RARGS+=(--config-rm "$OLD_ORCH")
docker service update "${RARGS[@]}" ai-router_router >/dev/null
for i in $(seq 1 40); do
curl -sf --max-time 5 https://ai-router.ift.calentiq.com/health >/dev/null && break
sleep 2
done
docker exec "$(docker ps -q -f name=ai-router_router | head -1)" \
grep -E 'executor_synthetic_mode|executor_no_tools_escalate' /app/config/orchestration.yaml
echo KICKSTART_NO_TOOLS_OK
REMOTE
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
PYTHONPATH=router python3 -m py_compile \
router/agent_hier.py router/router.py router/agent_stream.py \
router/path_resolve.py router/hierarchical.py
PYTHONPATH=router python3 -m unittest discover -s test/unit -q
scp router/agent_hier.py router/router.py router/agent_stream.py \
router/path_resolve.py router/hierarchical.py \
eventhub-ift:/opt/ai-router-stack/router/
scp config/orchestration.yaml eventhub-ift:/opt/ai-router-stack/config/
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
sed -i 's/\r$//' router/*.py config/orchestration.yaml || true
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
TS=$(date +%Y%m%d%H%M%S)
ORCH="orchestration_config_${TS}"
docker config create "$ORCH" ./config/orchestration.yaml
OLD_ORCH=$(docker service inspect ai-router_router --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep orchestration | tail -1 || true)
RARGS=(--image "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" --force)
RARGS+=(--config-add "source=${ORCH},target=/app/config/orchestration.yaml")
[[ -n "${OLD_ORCH:-}" ]] && RARGS+=(--config-rm "$OLD_ORCH")
docker service update "${RARGS[@]}" ai-router_router >/dev/null
for i in $(seq 1 40); do
curl -sf --max-time 5 https://ai-router.ift.calentiq.com/health >/dev/null && break
sleep 2
done
docker exec "$(docker ps -q -f name=ai-router_router | head -1)" \
grep -E 'path_resolve_' /app/config/orchestration.yaml
test -f /opt/ai-router-stack/router/path_resolve.py
echo PATH_RESOLVE_OK
REMOTE
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
PYTHONPATH=router python3 -m py_compile \
router/agent_hier.py router/router.py router/agent_stream.py \
router/path_resolve.py router/hierarchical.py router/progress_ui.py
PYTHONPATH=router python3 -m unittest discover -s test/unit -q
scp router/agent_hier.py router/router.py router/agent_stream.py \
router/path_resolve.py router/hierarchical.py router/progress_ui.py \
eventhub-ift:/opt/ai-router-stack/router/
scp config/orchestration.yaml eventhub-ift:/opt/ai-router-stack/config/
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
sed -i 's/\r$//' router/*.py config/orchestration.yaml || true
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
TS=$(date +%Y%m%d%H%M%S)
ORCH="orchestration_config_${TS}"
docker config create "$ORCH" ./config/orchestration.yaml
OLD_ORCH=$(docker service inspect ai-router_router --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep orchestration | tail -1 || true)
RARGS=(--image "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" --force)
RARGS+=(--config-add "source=${ORCH},target=/app/config/orchestration.yaml")
[[ -n "${OLD_ORCH:-}" ]] && RARGS+=(--config-rm "$OLD_ORCH")
docker service update "${RARGS[@]}" ai-router_router >/dev/null
for i in $(seq 1 40); do
curl -sf --max-time 5 https://ai-router.ift.calentiq.com/health >/dev/null && break
sleep 2
done
docker exec "$(docker ps -q -f name=ai-router_router | head -1)" \
grep -E 'executor_force_edit_after_read|path_resolve_enabled' /app/config/orchestration.yaml
test -f /opt/ai-router-stack/router/progress_ui.py
echo PROGRESS_UI_OK
REMOTE
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Hotfix: quiet progress + executor timeout/escalate
set -euo pipefail
AIR=/mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubAiRouter
cd "$AIR"
python3 -m py_compile router/hierarchical.py router/agent_stream.py router/router.py router/agent_hier.py
python3 -m unittest discover -s test/unit -q
PRIMARY_PROVIDER=hybrid LITELLM_MODEL_TIMEOUT=180 python3 scripts/gen-litellm-config.py
scp router/hierarchical.py router/agent_stream.py router/router.py router/agent_hier.py \
eventhub-ift:/opt/ai-router-stack/router/
scp config/orchestration.yaml eventhub-ift:/opt/ai-router-stack/config/
scp litellm_config.yaml eventhub-ift:/opt/ai-router-stack/litellm_config.yaml
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
sed -i 's/\r$//' router/*.py config/orchestration.yaml litellm_config.yaml || true
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
TS=$(date +%Y%m%d%H%M%S)
ORCH="orchestration_config_${TS}"
LITC="litellm_config_${TS}"
docker config create "$ORCH" ./config/orchestration.yaml
docker config create "$LITC" ./litellm_config.yaml
OLD_ORCH=$(docker service inspect ai-router_router --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep orchestration | tail -1 || true)
OLD_LIT=$(docker service inspect ai-router_litellm --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep litellm_config | grep -v entrypoint | tail -1 || true)
RARGS=(--image "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" --force)
RARGS+=(--config-add "source=${ORCH},target=/app/config/orchestration.yaml")
[[ -n "${OLD_ORCH:-}" ]] && RARGS+=(--config-rm "$OLD_ORCH")
docker service update "${RARGS[@]}" ai-router_router >/dev/null
LARGS=(--force --config-add "source=${LITC},target=/app/config.yaml")
[[ -n "${OLD_LIT:-}" ]] && LARGS+=(--config-rm "$OLD_LIT")
docker service update "${LARGS[@]}" ai-router_litellm >/dev/null
for i in $(seq 1 50); do
curl -sf --max-time 5 https://ai-router.ift.calentiq.com/health >/dev/null && \
curl -sf --max-time 5 https://litellm.ift.calentiq.com/health/liveliness >/dev/null && break
sleep 3
done
RID=$(docker ps -q -f name=ai-router_router | head -1)
LID=$(docker ps -q -f name=ai-router_litellm | head -1)
docker exec "$RID" grep -E 'progress_verbose|executor_timeout|executor_midloop_escalate|show_context_fill' /app/config/orchestration.yaml
docker exec "$LID" sh -c 'grep -A5 "model_name: a-medium-code" /app/config.yaml | head -8'
echo QUIET_PROGRESS_TIMEOUT_OK
REMOTE
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
PYTHONPATH=router python3 -m py_compile \
router/agent_hier.py router/router.py router/agent_stream.py \
router/path_resolve.py router/hierarchical.py router/progress_ui.py \
router/runtime_probe.py
PYTHONPATH=router python3 -m unittest discover -s test/unit -q
scp router/agent_hier.py router/router.py router/agent_stream.py \
router/path_resolve.py router/hierarchical.py router/progress_ui.py \
router/runtime_probe.py \
eventhub-ift:/opt/ai-router-stack/router/
scp config/orchestration.yaml eventhub-ift:/opt/ai-router-stack/config/
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
sed -i 's/\r$//' router/*.py config/orchestration.yaml || true
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
TS=$(date +%Y%m%d%H%M%S)
ORCH="orchestration_config_${TS}"
docker config create "$ORCH" ./config/orchestration.yaml
OLD_ORCH=$(docker service inspect ai-router_router --format '{{range .Spec.TaskTemplate.ContainerSpec.Configs}}{{println .ConfigName}}{{end}}' | grep orchestration | tail -1 || true)
RARGS=(--image "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" --force)
RARGS+=(--config-add "source=${ORCH},target=/app/config/orchestration.yaml")
[[ -n "${OLD_ORCH:-}" ]] && RARGS+=(--config-rm "$OLD_ORCH")
docker service update "${RARGS[@]}" ai-router_router >/dev/null
for i in $(seq 1 40); do
curl -sf --max-time 5 https://ai-router.ift.calentiq.com/health >/dev/null && break
sleep 2
done
docker exec "$(docker ps -q -f name=ai-router_router | head -1)" \
grep -E 'runtime_probe_enabled|path_resolve_enabled' /app/config/orchestration.yaml
test -f /opt/ai-router-stack/router/runtime_probe.py
echo RUNTIME_PROBE_OK
REMOTE
+38 -5
View File
@@ -23,21 +23,48 @@ ensure_secret() {
if docker secret inspect "$name" >/dev/null 2>&1; then
echo "secret exists: $name"
else
if [[ -z "$value" ]]; then
value="_"
fi
echo -n "$value" | docker secret create "$name" -
echo "created secret: $name"
fi
}
echo "== Ensure Swarm secrets =="
ensure_secret novita_api_key "${NOVITA_API_KEY:?NOVITA_API_KEY required}"
PRIMARY="${PRIMARY_PROVIDER:-hybrid}"
if [[ "$PRIMARY" == "hybrid" || "$PRIMARY" == "novita" ]]; then
ensure_secret novita_api_key "${NOVITA_API_KEY:?NOVITA_API_KEY required for PRIMARY_PROVIDER=${PRIMARY}}"
else
ensure_secret novita_api_key "${NOVITA_API_KEY:-}"
fi
ensure_secret anthropic_api_key "${ANTHROPIC_API_KEY:-}"
ensure_secret litellm_master_key "${LITELLM_MASTER_KEY:?LITELLM_MASTER_KEY required}"
ensure_secret litellm_salt_key "${LITELLM_SALT_KEY:?LITELLM_SALT_KEY required}"
ensure_secret router_api_key "${ROUTER_API_KEY:?ROUTER_API_KEY required}"
ensure_secret postgres_password "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required}"
ensure_secret groq_api_key "${GROQ_API_KEY:-}"
ensure_secret xai_api_key "${XAI_API_KEY:-}"
ensure_secret gemini_api_key "${GEMINI_API_KEY:-}"
ensure_secret together_api_key "${TOGETHER_API_KEY:-}"
ensure_secret openrouter_api_key "${OPENROUTER_API_KEY:-}"
ensure_secret gigachat_credentials "${GIGACHAT_CREDENTIALS:-}"
vless_conf_is_stub() {
local f="${1:-vless/vless.conf}"
[[ ! -f "$f" ]] && return 0
grep -qE "0\.0\.0\.0|example\.com|UUID@host|^#" "$f" && return 0
grep -qE "^vless://" "$f" || return 0
return 1
}
if [[ "${VPN_ENABLED:-false}" == "true" ]] && [[ -n "${VLESS_SUB_URL:-}" ]]; then
if vless_conf_is_stub "vless/vless.conf"; then
echo "== Fetch vless.conf from subscription =="
bash scripts/fetch-vless-subscription.sh vless/vless.conf
fi
fi
if [[ ! -f vless/vless.conf ]]; then
echo "WARN: vless/vless.conf missing — stub for secret (VPN off until configured)"
cp vless/vless.conf.example vless/vless.conf 2>/dev/null || echo "# stub" > vless/vless.conf
@@ -46,11 +73,17 @@ ensure_secret vless_conf "$(cat vless/vless.conf)"
echo "== Build images =="
docker build -f router/Dockerfile -t "${ROUTER_IMAGE:-git.sabilin.com/eventhub/ai-router-gateway:ift}" .
docker build -t ai-router/vless-proxy:local ./vless
docker build -t ai-router/vpn-watchdog:local ./watchdog
if [[ "${VPN_ENABLED:-false}" == "true" ]]; then
docker build -t ai-router/vless-proxy:local ./vless
docker build -t ai-router/vpn-watchdog:local ./watchdog
else
echo "VPN off — stub images (replicas=0)"
docker build -f vless/Dockerfile.stub -t ai-router/vless-proxy:local ./vless
docker build -t ai-router/vpn-watchdog:local ./watchdog
fi
echo "== Sync routing config (optional regen) =="
bash scripts/sync-routing-config.sh || true
echo "== Sync routing config (PRIMARY_PROVIDER=${PRIMARY_PROVIDER:-hybrid}) =="
python3 scripts/gen-litellm-config.py
echo "== Deploy stack: ${STACK_NAME} =="
docker stack deploy -c docker-stack.yml --with-registry-auth "${STACK_NAME}"
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== litellm 400/408 detail =="
docker service logs --since 40m --raw ai-router_litellm 2>&1 \
| grep -iE 'BadRequest|invalid request|400|deepseek|tool|error|trace_id' \
| tail -80
echo
echo "== router 400 bodies =="
docker service logs --since 40m --raw ai-router_router 2>&1 \
| grep -iE '400|BadRequest|invalid|executor failed|mid-loop' \
| tail -40
REMOTE
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== Spend 15m =="
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT \"startTime\", status, model, total_tokens, completion_tokens,
coalesce(\"request_duration_ms\",0) AS ms,
left(coalesce(metadata->'error_information'->>'error_message',''), 100) AS er
FROM \"LiteLLM_SpendLogs\"
WHERE \"startTime\" >= now() - interval '20 minutes'
ORDER BY \"startTime\" DESC LIMIT 25;
"
echo "== Router =="
docker service logs --since 20m --raw ai-router_router 2>&1 \
| grep -iE 'path_index|path_resolve|hierarchical_agent phase|executor result|408|escalate|mid-loop|rewrite|kickstart|loadtest|dynamic_conf' \
| tail -60
REMOTE
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
docker service logs --since 20m --raw ai-router_router 2>&1 \
| grep -iE 'docker-compose|kickstart|tool_calls|executor result|rewrite|hierarchical_agent phase|not found' \
| tail -50
REMOTE
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== Spend 20m =="
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT to_char(\"startTime\", 'HH24:MI:SS') AS t, status,
CASE WHEN model LIKE '%qwen3.8-max%' THEN 'Max'
WHEN model LIKE '%qwen3-coder%' THEN 'Coder'
WHEN model LIKE '%deepseek%' THEN 'DeepSeek'
ELSE left(model,28) END AS role,
total_tokens AS tok, completion_tokens AS out,
coalesce(\"request_duration_ms\",0) AS ms
FROM \"LiteLLM_SpendLogs\"
WHERE \"startTime\" >= now() - interval '20 minutes'
ORDER BY \"startTime\" ASC;
"
echo
echo "== Router (edit/force/loop) =="
docker service logs --since 20m --raw ai-router_router 2>&1 \
| grep -iE 'force edit|edit_file|rewrite|path_index|hierarchical_agent phase|executor result|synth|sticky|Стоп|fallback|tool_calls|calentiq-ift' \
| grep -vE 'metrics|health|Waiting|Uvicorn|Started|Finished|Shutting' \
| tail -80
REMOTE
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
echo "== Spend last 30m =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT \"startTime\", status, model, call_type, total_tokens,
left(coalesce(metadata->'error_information'->>'error_message',''), 180) AS er
FROM \"LiteLLM_SpendLogs\"
ORDER BY \"startTime\" DESC
LIMIT 25;
"
echo "== Router logs (executor/plan) =="
docker service logs --since 25m ai-router_router 2>&1 \
| grep -iE 'hierarchical|executor|novita-planner|a-medium-code|timeout|408|error|fail|tool' \
| tail -50
echo "== LiteLLM logs errors =="
docker service logs --since 25m ai-router_litellm 2>&1 \
| grep -iE 'ERROR|Exception|timeout|408|502|qwen3-coder|novita-planner|a-medium-code|Give Feedback' \
| tail -40
REMOTE
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
docker service logs --since 25m --raw ai-router_router 2>&1 \
| grep -iE 'traefik\.yml|rewrite|bogus|kickstart|executor result|hierarchical_agent phase|paths:|Plan approved|tool_calls' \
| tail -60
REMOTE
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
echo "== live router logs (last 5m, full) =="
docker service logs --since 8m --raw ai-router_router 2>&1 | tail -80
echo
echo "== litellm last =="
docker service logs --since 8m --raw ai-router_litellm 2>&1 | grep -iE 'qwen|deepseek|timeout|408|error|POST' | tail -40
echo
echo "== in-flight? =="
docker service ps ai-router_router --no-trunc | head -5
REMOTE
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== Spend after 19:06 =="
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT \"startTime\", status, model, total_tokens, completion_tokens,
coalesce(\"request_duration_ms\",0) AS ms,
left(coalesce(metadata->'error_information'->>'error_message',''), 120) AS er
FROM \"LiteLLM_SpendLogs\"
WHERE \"startTime\" >= '2026-08-12 19:06:00'
ORDER BY \"startTime\" DESC;
"
echo "== router since 19:06 full agent lines =="
docker service logs --since 15m --raw ai-router_router 2>&1 \
| grep -iE '19:0[6-9]|19:1|executor|tool_loop|408|HTTP Request|kickstart|escalate|fallback|synth|no tool|POST /v1' \
| tail -60
REMOTE
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== Services =="
docker stack services ai-router --format '{{.Name}} {{.Replicas}} {{.Image}}' | grep -E 'litellm|postgres|router' || true
echo "== Health =="
curl -sS -o /tmp/llh.txt -w "liveliness:%{http_code}\n" --max-time 10 https://litellm.ift.calentiq.com/health/liveliness || true
curl -sS -o /tmp/llr.txt -w "readiness:%{http_code}\n" --max-time 10 https://litellm.ift.calentiq.com/health/readiness || true
head -c 300 /tmp/llh.txt; echo; head -c 500 /tmp/llr.txt; echo
echo "== Recent litellm logs (errors) =="
docker service logs --tail 120 ai-router_litellm 2>&1 \
| grep -iE 'error|exception|fail|traceback|500|401|prisma|database|budget|key|warning' \
| tail -60 || true
echo "== Last 40 lines raw =="
docker service logs --tail 40 ai-router_litellm 2>&1 | tail -40
echo "== Postgres quick =="
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c \
"SELECT count(*) AS spend FROM \"LiteLLM_SpendLogs\"; SELECT count(*) AS users FROM \"LiteLLM_UserTable\"; SELECT count(*) AS tokens FROM \"LiteLLM_VerificationToken\";"
echo "== UI/home probe =="
curl -sS -o /tmp/ui.txt -w "ui:%{http_code}\n" --max-time 15 https://litellm.ift.calentiq.com/ui/ || true
curl -sS -o /tmp/models.txt -w "models:%{http_code}\n" --max-time 20 \
-H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
https://litellm.ift.calentiq.com/v1/models || true
head -c 200 /tmp/models.txt; echo
REMOTE
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
echo "== SpendLogs status breakdown =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT status, count(*) FROM \"LiteLLM_SpendLogs\" GROUP BY 1 ORDER BY 2 DESC;
"
echo "== Recent spend rows =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT \"startTime\", status, model, \"api_key\",
left(coalesce(\"error_information\"::text, ''), 120) AS err,
left(coalesce(request_tags::text, ''), 60) AS tags
FROM \"LiteLLM_SpendLogs\"
ORDER BY \"startTime\" DESC
LIMIT 20;
"
echo "== ErrorLogs =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c \
"SELECT count(*) FROM \"LiteLLM_ErrorLogs\";"
echo "== Body-read errors around restart =="
docker service logs --since 15m ai-router_litellm 2>&1 \
| grep -iE 'Unexpected error reading request body|ClientDisconnect|ConnectionReset|502|504|failed' \
| tail -30 || true
echo "== Uptime-kuma / probes hitting litellm =="
docker service logs --tail 30 ai-router_uptime-kuma 2>&1 | tail -20 || true
REMOTE
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
USER="${POSTGRES_USER:-litellm}"
DB="${POSTGRES_DB:-litellm}"
echo "== SpendLogs columns =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "$USER" -d "$DB" -c "\d+ \"LiteLLM_SpendLogs\"" | head -80
echo "== Recent failures =="
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "$USER" -d "$DB" -c "
SELECT \"startTime\", status, model,
left(coalesce(messages::text,''), 80) AS messages,
left(coalesce(metadata::text,''), 200) AS metadata,
left(coalesce(\"proxyServerRequest\"::text,''), 120) AS req,
left(coalesce(response::text,''), 200) AS response
FROM \"LiteLLM_SpendLogs\"
ORDER BY \"startTime\" DESC
LIMIT 15;
"
REMOTE
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT \"startTime\", status, model, call_type,
left(coalesce(metadata::text,''), 400) AS metadata,
left(coalesce(response::text,''), 300) AS response
FROM \"LiteLLM_SpendLogs\"
ORDER BY \"startTime\" DESC
LIMIT 15;
"
REMOTE
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== Spend last 15m =="
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT \"startTime\", status, model, call_type, total_tokens, completion_tokens,
left(coalesce(metadata->'error_information'->>'error_message',''), 120) AS er
FROM \"LiteLLM_SpendLogs\"
ORDER BY \"startTime\" DESC LIMIT 15;
"
echo "== Router =="
docker service logs --since 15m ai-router_router 2>&1 \
| grep -iE 'executor|tool_calls|finish|no_tool|408|escalate|hierarchical_agent|a-medium-code|b-complex' \
| tail -40
REMOTE
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
echo "== curl inside from traefik net =="
# find traefik containe
TR=$(docker ps --filter name=traefik -q | head -1)
OW=$(docker ps --filter name=observer_web -q | head -1)
echo "traefik=$TR observer=$OW"
if [[ -n "$OW" ]]; then
echo "-- observer logs --"
docker logs --tail 30 "$OW" 2>&1 | tail -30
echo "-- wget localhost from observer --"
docker exec "$OW" sh -c 'wget -qO- --timeout=3 http://127.0.0.1/ 2>&1 | head -c 200; echo; wget -qO- --timeout=3 http://127.0.0.1:80/ 2>&1 | head -c 200; echo' || true
echo "-- inspect ports --"
docker inspect "$OW" --format 'IP={{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} Ports={{json .NetworkSettings.Ports}}'
fi
if [[ -n "$TR" && -n "$OW" ]]; then
echo "-- traefik -> observer_web:80 --"
docker exec "$TR" wget -qO- --timeout=5 http://observer_web:80/ 2>&1 | head -c 300 || \
docker exec "$TR" wget -qO- --timeout=5 http://eventhub-ift-core_observer_web:80/ 2>&1 | head -c 300 || true
echo
# DNS from traefik
docker exec "$TR" getent hosts observer_web 2>&1 || docker exec "$TR" nslookup observer_web 2>&1 | head -10 || true
fi
echo "== dynamic_conf observer snippet =="
grep -n -A6 'observer' /opt/ai-router-stack/../EventHubDevOps/ift/traefik/dynamic_conf.yml 2>/dev/null | head -5 || \
grep -n -A5 'observer:' /home/*/IdeaProjects/eventHub/EventHubDevOps/ift/traefik/dynamic_conf.yml 2>/dev/null | head || true
# try find compose on host
find /opt -name 'dynamic_conf.yml' 2>/dev/null | head -5
REMOTE
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== Spend last 20m =="
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT \"startTime\", status, model, total_tokens, completion_tokens,
coalesce(\"request_duration_ms\",0) AS ms,
left(coalesce(metadata->'error_information'->>'error_message',''), 100) AS er
FROM \"LiteLLM_SpendLogs\"
ORDER BY \"startTime\" DESC LIMIT 20;
"
echo "== Router recent =="
docker service logs --since 20m ai-router_router 2>&1 \
| grep -iE 'executor|tool_loop|kickstart|escalate|408|no tool|hierarchical_agent|phase=|waiting|timeout' \
| tail -50
REMOTE
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
echo "== Spend last 25m =="
PG=$(docker ps -q -f name=ai-router_postgres | head -1)
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$PG" \
psql -U "${POSTGRES_USER:-litellm}" -d "${POSTGRES_DB:-litellm}" -c "
SELECT to_char(\"startTime\", 'HH24:MI:SS') AS t, status,
CASE
WHEN model LIKE '%qwen3.8-max%' THEN 'planner-Max'
WHEN model LIKE '%qwen3-coder%' THEN 'coder-30b'
WHEN model LIKE '%deepseek%' THEN 'DeepSeek'
WHEN model LIKE '%llama%' THEN 'Llama'
ELSE left(model, 40)
END AS role,
total_tokens AS tok, completion_tokens AS out,
coalesce(\"request_duration_ms\",0) AS ms,
left(coalesce(metadata->'error_information'->>'error_message',''), 80) AS er
FROM \"LiteLLM_SpendLogs\"
WHERE \"startTime\" >= now() - interval '25 minutes'
ORDER BY \"startTime\" ASC;
"
echo
echo "== Router agent timeline =="
docker service logs --since 25m --raw ai-router_router 2>&1 \
| grep -iE 'path_index|path_resolve|hierarchical_agent phase|executor forward|executor result|escalate|408|400|rewrite|kickstart|no tool|mid-loop|synth|Выполн|plan' \
| grep -vE 'metrics|health|Waiting for|Application|Uvicorn|Finished server|Started server|Shutting' \
| tail -80
REMOTE
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Fetch vless.conf from Happ/mireon subscription (JSON or plain vless:// lines).
# Env: VLESS_SUB_URL, VLESS_UA, VLESS_HWID (Happ → Settings / app id)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
if [[ -f "${ROOT}/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "${ROOT}/.env"
set +a
fi
SUB_URL="${VLESS_SUB_URL:?set VLESS_SUB_URL}"
OUT="${1:-${ROOT}/vless/vless.conf}"
UA="${VLESS_UA:-Happ/3.3.6/Windows/2607171516600}"
HWID="${VLESS_HWID:-}"
TMP="$(mktemp)"
trap 'rm -f "${TMP}"' EXIT
CURL=(curl -fsSL -A "$UA")
[[ -n "$HWID" ]] && CURL+=(-H "x-hwid: ${HWID}")
"${CURL[@]}" "$SUB_URL" | tr -d '\n\r' | base64 -d >"${TMP}" 2>/dev/null || "${CURL[@]}" "$SUB_URL" >"${TMP}"
pick="$(grep -E '^vless://' "${TMP}" | grep -v '@0\.0\.0\.0:1' | head -1 || true)"
if [[ -z "$pick" ]]; then
pick="$(python3 "${SCRIPT_DIR}/vless-from-subscription-json.py" "${TMP}")" || true
fi
if [[ -z "$pick" ]]; then
echo "fetch-vless-subscription: no valid vless URI" >&2
head -c 400 "${TMP}" >&2
exit 1
fi
mkdir -p "$(dirname "$OUT")"
printf '%s\n' "$pick" >"$OUT"
chmod 600 "$OUT"
echo "Wrote $(wc -c <"$OUT") bytes -> $OUT"
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""Generate litellm_config.yaml from PRIMARY_PROVIDER + config/*.yaml."""
from __future__ import annotations
import os
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parent.parent
PROFILE = os.environ.get("PRIMARY_PROVIDER", "hybrid").strip().lower()
def load(path: Path) -> dict:
with path.open(encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def api_key_ref(name: str) -> str:
return f"os.environ/{name}"
def _apply_thinking(params: dict, spec: dict) -> None:
"""Qwen3 defaults to thinking; empty content + hang unless disabled."""
model = str(params.get("model") or spec.get("model") or "").lower()
force = bool(spec.get("disable_thinking"))
auto = ("qwen3" in model) and not spec.get("enable_thinking")
if force or auto:
params["extra_body"] = {
"chat_template_kwargs": {"enable_thinking": False},
"enable_thinking": False,
}
def lane_entry(name: str, spec: dict, rpm: int | None) -> dict:
params: dict = {
"model": spec["model"],
"api_key": api_key_ref(spec["api_key"]),
# Per-deployment timeout: litellm_settings.request_timeout often shows as
# Deployment Info timeout: None on stream/tool hangs.
"timeout": int(os.environ.get("LITELLM_MODEL_TIMEOUT", "180")),
}
if rpm:
params["rpm"] = rpm
_apply_thinking(params, spec)
return {"model_name": name, "litellm_params": params}
def fixed_entry(name: str, spec: dict) -> dict:
params: dict = {
"model": spec["model"],
"api_key": api_key_ref(spec["api_key"]),
"timeout": int(os.environ.get("LITELLM_MODEL_TIMEOUT", "180")),
}
for key in ("ssl_verify", "max_tokens", "temperature"):
if key in spec:
params[key] = spec[key]
_apply_thinking(params, spec)
return {"model_name": name, "litellm_params": params}
def smart_router_internal(rules: dict) -> dict:
litellm_rules = rules.get("litellm", {})
return {
"model_name": "smart-router-internal",
"litellm_params": {
"model": "auto_router/complexity_router",
"drop_params": True,
"complexity_router_default_model": "a-medium-ops",
"complexity_router_config": {
"tiers": {
"SIMPLE": "a-simple",
"MEDIUM": "a-medium-ops",
"MEDIUM_CODE": "a-medium-code",
"COMPLEX": "a-complex",
"REASONING": "a-reasoning",
},
"classifier_fallback": "heuristic",
"keyword_tier_rules": litellm_rules.get("keyword_tier_rules", []),
"custom_technical_keywords": litellm_rules.get(
"custom_technical_keywords", []
),
"token_thresholds": {"simple": 20, "complex": 500},
"tier_boundaries": {
"simple_medium": 0.18,
"medium_complex": 0.38,
"complex_reasoning": 0.62,
},
"session_affinity": True,
"session_affinity_ttl_seconds": 1800,
},
},
}
def main() -> int:
providers = load(ROOT / "config/providers.yaml")
profiles = providers.get("profiles", {})
if PROFILE not in profiles:
print(f"ERROR: unknown PRIMARY_PROVIDER={PROFILE!r}", file=sys.stderr)
print(f"Available: {', '.join(sorted(profiles))}", file=sys.stderr)
return 1
profile = profiles[PROFILE]
base = load(ROOT / "config/litellm.base.yaml")
matrix = load(ROOT / "config/model_matrix.yaml")
rules = load(ROOT / "config/routing_rules.yaml")
meta = matrix.get("models", {})
model_list: list[dict] = []
# Classifier first
fixed = providers.get("fixed_models", {})
if "gigachat-classifier" in fixed:
model_list.append(fixed_entry("gigachat-classifier", fixed["gigachat-classifier"]))
# Lane models from active profile
lanes = profile.get("lanes", {})
for name, spec in lanes.items():
rpm = meta.get(name, {}).get("rpm")
model_list.append(lane_entry(name, spec, rpm))
# Fixed models (Claude / OR fallbacks / GigaChat / optional free tiers)
for name in (
"novita-planner",
"novita-verifier",
"claude-haiku-planner",
"claude-sonnet-verifier",
"groq-llama-8b",
"groq-qwen-coder",
"gemini-flash",
"grok-3",
"gigachat-pro",
):
if name in fixed:
model_list.append(fixed_entry(name, fixed[name]))
model_list.append(smart_router_internal(rules))
# smart-router alias (Zed default)
sr = profile.get("smart_router", {})
if sr:
sr_params: dict = {
"model": sr["model"],
"api_key": api_key_ref(sr["api_key"]),
}
_apply_thinking(sr_params, sr)
model_list.append({"model_name": "smart-router", "litellm_params": sr_params})
router = base.setdefault("router_settings", {})
router["fallbacks"] = [
{k: v for k, v in row.items()}
for row in _fallback_list(profile.get("fallbacks", {}))
]
router["default_fallbacks"] = profile.get(
"default_fallbacks", ["a-medium-code", "groq-qwen-coder"]
)
out_cfg = {**base, "model_list": model_list}
out_path = ROOT / "litellm_config.yaml"
header = (
f"# LiteLLM — generated for PRIMARY_PROVIDER={PROFILE}\n"
f"# Profile: {profile.get('label', PROFILE)}\n"
f"# Regenerate: PRIMARY_PROVIDER={PROFILE} bash scripts/gen-litellm-config.py\n\n"
)
body = yaml.dump(out_cfg, allow_unicode=True, sort_keys=False)
out_path.write_text(header + body, encoding="utf-8")
print(f"Wrote {out_path} (profile={PROFILE}, {len(model_list)} models)")
return 0
def _fallback_list(fallbacks: dict) -> list[dict]:
return [{model: targets} for model, targets in fallbacks.items()]
if __name__ == "__main__":
sys.exit(main())
+12
View File
@@ -20,9 +20,21 @@ fi
if [ -f /run/secrets/groq_api_key ]; then
export GROQ_API_KEY="$(read_secret /run/secrets/groq_api_key)"
fi
if [ -f /run/secrets/xai_api_key ]; then
export XAI_API_KEY="$(read_secret /run/secrets/xai_api_key)"
fi
if [ -f /run/secrets/gemini_api_key ]; then
export GEMINI_API_KEY="$(read_secret /run/secrets/gemini_api_key)"
fi
if [ -f /run/secrets/together_api_key ]; then
export TOGETHER_API_KEY="$(read_secret /run/secrets/together_api_key)"
fi
if [ -f /run/secrets/openrouter_api_key ]; then
export OPENROUTER_API_KEY="$(read_secret /run/secrets/openrouter_api_key)"
fi
if [ -f /run/secrets/anthropic_api_key ]; then
export ANTHROPIC_API_KEY="$(read_secret /run/secrets/anthropic_api_key)"
fi
if [ -f /run/secrets/gigachat_credentials ]; then
export GIGACHAT_CREDENTIALS="$(read_secret /run/secrets/gigachat_credentials)"
fi
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Canonical NO_PROXY when litellm uses HTTP_PROXY (VLESS VPN).
#
# Direct (bypass VPN): Novita, Groq, Gemini, GigaChat, internal/Calentiq.
# VPN-only (NOT listed): Anthropic → api.anthropic.com; OpenRouter → openrouter.ai;
# xAI Grok → api.x.ai; Together → api.together.xyz
#
# shellcheck disable=SC2034
NO_PROXY_DEFAULT='localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,api.novita.ai,novita.ai,api.groq.com,groq.com,generativelanguage.googleapis.com,ngw.devices.sberbank.ru,gigachat.devices.sberbank.ru,git.sabilin.com,*.ift.calentiq.com,ift.calentiq.com,*.ift.eventhub.local,*.eventhub.local'
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Refresh vless.conf from subscription and reload vless-proxy on Swarm (IFT).
set -euo pipefail
STACK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STACK_NAME="${STACK_NAME:-ai-router}"
cd "$STACK_DIR"
set -a
# shellcheck disable=SC1091
source .env
set +a
bash "${STACK_DIR}/scripts/fetch-vless-subscription.sh" "${STACK_DIR}/vless/vless.conf"
CONF="$(cat "${STACK_DIR}/vless/vless.conf")"
SVC="${STACK_NAME}_vless-proxy"
rotate_vless_secret() {
if docker secret inspect vless_conf >/dev/null 2>&1; then
echo "== Detach old vless_conf secret =="
docker service scale "${SVC}=0" >/dev/null || true
sleep 3
docker service update --secret-rm vless_conf "${SVC}" >/dev/null 2>&1 || true
sleep 2
docker secret rm vless_conf >/dev/null 2>&1 || true
fi
echo -n "$CONF" | docker secret create vless_conf -
}
if docker service inspect "${SVC}" >/dev/null 2>&1; then
rotate_vless_secret
docker service update \
--secret-add "source=vless_conf,target=/app/vless.conf,mode=0444" \
"${SVC}" >/dev/null
docker service update --force "${SVC}" >/dev/null
TARGET="${VPN_ENABLED:-false}"
if [[ "$TARGET" == "true" ]]; then
docker service scale "${SVC}=1" >/dev/null
fi
echo "refresh-vless: vless-proxy reloaded"
else
rotate_vless_secret
echo "refresh-vless: secret vless_conf updated (service ${SVC} missing — run deploy.sh)"
fi
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Cron: refresh VPN subscription every 6h (config может меняться на стороне провайдера).
set -euo pipefail
STACK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CRON_FILE="/etc/cron.d/ai-router-vless-refresh"
LOG="/var/log/ai-router-vless-refresh.log"
sudo tee "$CRON_FILE" >/dev/null <<EOF
# AI Router: refresh Happ/mireon vless subscription
0 */6 * * * deploy cd ${STACK_DIR} && set -a && . ./.env && set +a && bash scripts/refresh-vless-subscription.sh >>${LOG} 2>&1
EOF
sudo chmod 644 "$CRON_FILE"
echo "Installed ${CRON_FILE} (every 6h)"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
BASE="${PUBLIC_URL:-https://ai-router.ift.calentiq.com}"
KEY="${ROUTER_API_KEY:-$LITELLM_MASTER_KEY}"
PLAN=$(python3 - <<'PY'
import json
d=json.load(open("/tmp/agent-plan.json"))
msg=(d.get("choices") or [{}])[0].get("message") or {}
print(msg.get("content") or "")
PY
)
# Approve with tools like Zed agent
python3 - <<'PY' > /tmp/agent-approve-req.json
import json
plan=open("/tmp/agent-plan.json").read()
d=json.loads(plan)
content=((d.get("choices") or [{}])[0].get("message") or {}).get("content") or ""
tools=[{
"type":"function",
"function":{
"name":"read_file",
"description":"Read a file",
"parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}
}
},{
"type":"function",
"function":{
"name":"edit_file",
"description":"Edit a file",
"parameters":{"type":"object","properties":{
"path":{"type":"string"},
"edits":{"type":"array","items":{"type":"object","properties":{
"old_text":{"type":"string"},"new_text":{"type":"string"}
}}}
},"required":["path","edits"]}
}
}]
body={
"model":"smart-router",
"stream": False,
"tools": tools,
"tool_choice":"auto",
"messages":[
{"role":"user","content":"observer.ift.calentiq.com не работает - Bad Gateway почини"},
{"role":"assistant","content":content},
{"role":"user","content":"ok"}
]
}
json.dump(body, open("/tmp/agent-approve-req.json","w"), ensure_ascii=False)
print("req bytes", len(json.dumps(body)))
PY
echo "== approve+tools =="
curl -sS --max-time 180 "$BASE/v1/chat/completions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "X-AI-Orchestrate: force" \
-H "X-AI-Quality: balanced" \
-d @/tmp/agent-approve-req.json | tee /tmp/agent-exec1.json | python3 -c '
import json,sys
d=json.load(sys.stdin)
ch=(d.get("choices") or [{}])[0]
msg=ch.get("message") or {}
print("finish", ch.get("finish_reason"))
print("content", (msg.get("content") or "")[:1200])
tcs=msg.get("tool_calls") or []
print("tool_calls", len(tcs))
for tc in tcs:
fn=tc.get("function") or {}
print(" TOOL", fn.get("name"), (fn.get("arguments") or "")[:300])
meta=d.get("x_router_meta") or {}
print("meta phase", meta.get("agent_phase"), "force_edit", meta.get("executor_force_edit_after_read"), "model", meta.get("executor_model") or meta.get("selected_model"))
'
echo "== router log since approve =="
docker service logs --since 3m --raw ai-router_router 2>&1 \
| grep -iE 'path_index|force edit|stop after|hierarchical_agent|executor result|kickstart|phase=' \
| tail -30
REMOTE
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift 'bash -s' <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
BASE="${PUBLIC_URL:-https://ai-router.ift.calentiq.com}"
KEY="${ROUTER_API_KEY:-$LITELLM_MASTER_KEY}"
test -f /tmp/agent-plan.json && echo "plan_ok $(wc -c </tmp/agent-plan.json)" || echo "no plan"
python3 <<'PY'
import json
d=json.load(open("/tmp/agent-plan.json"))
content=((d.get("choices") or [{}])[0].get("message") or {}).get("content") or ""
tools=[{
"type":"function",
"function":{
"name":"read_file",
"description":"Read a file",
"parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}
}
},{
"type":"function",
"function":{
"name":"edit_file",
"description":"Edit a file",
"parameters":{"type":"object","properties":{
"path":{"type":"string"},
"edits":{"type":"array","items":{"type":"object","properties":{
"old_text":{"type":"string"},"new_text":{"type":"string"}
}}}
},"required":["path","edits"]}
}
}]
body={
"model":"smart-router",
"stream": False,
"tools": tools,
"messages":[
{"role":"user","content":"observer.ift.calentiq.com не работает - Bad Gateway почини"},
{"role":"assistant","content":content},
{"role":"user","content":"ok"}
]
}
json.dump(body, open("/tmp/agent-approve-req.json","w"), ensure_ascii=False)
print("req_ok", len(content), "tools", len(tools))
PY
code=$(curl -sS --max-time 180 -o /tmp/agent-exec1.json -w "%{http_code}" \
"$BASE/v1/chat/completions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "X-AI-Orchestrate: force" \
-H "X-AI-Quality: balanced" \
-d @/tmp/agent-approve-req.json || true)
echo "http=$code bytes=$(wc -c </tmp/agent-exec1.json)"
head -c 500 /tmp/agent-exec1.json; echo
python3 <<'PY'
import json
raw=open("/tmp/agent-exec1.json").read().strip()
if not raw:
print("EMPTY"); raise SystemExit
d=json.loads(raw)
if d.get("error"):
print("ERROR", d["error"]); raise SystemExit
ch=(d.get("choices") or [{}])[0]
msg=ch.get("message") or {}
print("finish", ch.get("finish_reason"))
print("content_head:")
print((msg.get("content") or "")[:1500])
tcs=msg.get("tool_calls") or []
print("n_tools", len(tcs))
for tc in tcs:
fn=tc.get("function") or {}
print("TOOL", fn.get("name"))
print("ARGS", (fn.get("arguments") or "")[:400])
meta=d.get("x_router_meta") or {}
print("phase", meta.get("agent_phase"), "exec", meta.get("executor_model") or meta.get("selected_model"),
"force", meta.get("executor_force_edit_after_read"), "stop", meta.get("executor_stop_after_edit"))
PY
docker service logs --since 4m --raw ai-router_router 2>&1 \
| grep -iE 'path_index|force edit|stop after|phase=|executor result|kickstart|Выполн' \
| tail -25
REMOTE
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
ssh -o BatchMode=yes eventhub-ift bash -s <<'REMOTE'
set -euo pipefail
cd /opt/ai-router-stack
set -a; source .env; set +a
BASE="${PUBLIC_URL:-https://ai-router.ift.calentiq.com}"
KEY="${ROUTER_API_KEY:-$LITELLM_MASTER_KEY}"
echo "BASE=$BASE"
# 1) Plan request (no tools) — hierarchical plan confirm
curl -sS --max-time 120 "$BASE/v1/chat/completions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "X-AI-Orchestrate: force" \
-H "X-AI-Quality: balanced" \
-d '{
"model": "smart-router",
"stream": false,
"messages": [
{"role": "user", "content": "observer.ift.calentiq.com не работает - Bad Gateway почини"}
]
}' | tee /tmp/agent-plan.json | python3 -c '
import json,sys
d=json.load(sys.stdin)
msg=(d.get("choices") or [{}])[0].get("message") or {}
print("=== PLAN CONTENT ===")
print((msg.get("content") or "")[:2500])
meta=d.get("x_router_meta") or {}
print("=== META keys ===", sorted(meta.keys())[:30])
print("mode", meta.get("mode"), "awaiting", meta.get("awaiting_plan_confirm"), "planner", meta.get("planner_model"))
'
REMOTE
+36 -6
View File
@@ -14,9 +14,28 @@ BASE="${TEST_BASE_URL:-http://127.0.0.1:8000}"
LITELLM_BASE="${TEST_LITELLM_URL:-http://127.0.0.1:4000}"
ROUTER_KEY="${ROUTER_API_KEY:?ROUTER_API_KEY required}"
CURL=(curl -sf)
if [[ "$BASE" == https:* ]]; then
CURL+=( -k )
curl_common() {
local url="$1"
CURL=(curl -sf)
if [[ "$url" == https:* ]]; then
CURL+=( -k )
fi
if [[ -n "${TEST_RESOLVE_IP:-}" ]]; then
local host="${url#*://}"
host="${host%%/*}"
host="${host%%:*}"
CURL+=( --resolve "${host}:443:${TEST_RESOLVE_IP}" --resolve "${host}:80:${TEST_RESOLVE_IP}" )
fi
}
curl_common "${BASE}"
if [[ "$LITELLM_BASE" == https:* && -n "${TEST_RESOLVE_IP:-}" ]]; then
LITELLM_CURL=(curl -sf -k)
lh="${LITELLM_BASE#*://}"; lh="${lh%%/*}"; lh="${lh%%:*}"
LITELLM_CURL+=( --resolve "${lh}:443:${TEST_RESOLVE_IP}" --resolve "${lh}:80:${TEST_RESOLVE_IP}" )
else
LITELLM_CURL=(curl -sf)
[[ "$LITELLM_BASE" == https:* ]] && LITELLM_CURL+=( -k )
fi
echo "== Router health =="
@@ -34,20 +53,31 @@ echo "== Classify COMPLEX =="
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Спроектируй архитектуру microservices"}]}' | jq .
echo "== Chat smart-router max_tokens=16 =="
echo "== Chat smart-router max_tokens=16 (single-shot / no force hierarchical) =="
"${CURL[@]}" "${BASE}/v1/chat/completions" \
-H "Authorization: Bearer ${ROUTER_KEY}" \
-H "Content-Type: application/json" \
-H "X-AI-Orchestrate: off" \
-d '{"model":"smart-router","max_tokens":16,"messages":[{"role":"user","content":"bash docker service ls"}]}' \
| jq '.choices[0].message.content, .x_router_meta // empty'
if [[ "${SMOKE_HIERARCHICAL:-}" == "1" ]]; then
echo "== Hierarchical force (Novita plan/verify + workers; VPN not required) =="
"${CURL[@]}" "${BASE}/v1/chat/completions" \
-H "Authorization: Bearer ${ROUTER_KEY}" \
-H "Content-Type: application/json" \
-H "X-AI-Orchestrate: force" \
-d '{"model":"smart-router","max_tokens":256,"messages":[{"role":"user","content":"Разбей на 2 шага: 1) что такое Docker 2) одна команда docker ps. Кратко."}]}' \
| jq '.x_router_meta.mode, .x_router_meta.planner_model, .x_router_meta.worker_calls, .x_router_meta.verify_skipped, (.choices[0].message.content|.[0:200])'
fi
if [[ -n "${SKIP_LITELLM_SMOKE:-}" ]]; then
echo "SKIP_LITELLM_SMOKE set — skipping LiteLLM checks"
else
echo "== LiteLLM liveliness =="
"${CURL[@]}" "${LITELLM_BASE}/health/liveliness" && echo
"${LITELLM_CURL[@]}" "${LITELLM_BASE}/health/liveliness" && echo
echo "== LiteLLM UI =="
curl -sfI -k "${LITELLM_BASE}/ui" | head -3 || true
"${LITELLM_CURL[@]}" -I "${LITELLM_BASE}/ui" | head -3 || true
fi
echo "All smoke checks passed."
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Switch PRIMARY_PROVIDER and regenerate litellm_config.yaml
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
PROFILE="${1:-}"
if [[ -z "$PROFILE" ]]; then
echo "Usage: bash scripts/switch-provider.sh <profile>" >&2
echo "Profiles:" >&2
python3 -c "
import yaml
from pathlib import Path
p = yaml.safe_load(Path('config/providers.yaml').read_text(encoding='utf-8'))
for k, v in sorted(p.get('profiles', {}).items()):
wc = ' [welcome credit]' if v.get('welcome_credit') else ''
print(f' {k:12} {v.get(\"label\", \"\")}{wc}')
"
exit 1
fi
if [[ ! -f .env ]]; then
echo "ERROR: .env missing — copy from .env.example" >&2
exit 1
fi
if grep -q '^PRIMARY_PROVIDER=' .env; then
sed -i "s/^PRIMARY_PROVIDER=.*/PRIMARY_PROVIDER=${PROFILE}/" .env
else
echo "PRIMARY_PROVIDER=${PROFILE}" >> .env
fi
set -a
# shellcheck disable=SC1091
source .env
set +a
python3 scripts/gen-litellm-config.py
echo ""
echo "Switched to PRIMARY_PROVIDER=${PROFILE}"
echo "Redeploy on IFT: bash scripts/deploy.sh (Swarm configs are immutable — stack rm if deploy fails)"
+9 -75
View File
@@ -1,78 +1,12 @@
#!/usr/bin/env bash
# Sync model_list fragment from config/*.yaml
# Regenerate litellm_config.yaml from PRIMARY_PROVIDER + config/providers.yaml
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export ROOT
python3 <<'PY'
import os
import yaml
from pathlib import Path
root = Path(os.environ["ROOT"])
rules = yaml.safe_load((root / "config/routing_rules.yaml").read_text(encoding="utf-8"))
matrix = yaml.safe_load((root / "config/model_matrix.yaml").read_text(encoding="utf-8"))
entries = []
for name, cfg in matrix.get("models", {}).items():
novita = cfg.get("novita")
if not novita:
continue
entry = {
"model_name": name,
"litellm_params": {
"model": novita,
"api_key": "os.environ/NOVITA_API_KEY",
},
}
if cfg.get("rpm"):
entry["litellm_params"]["rpm"] = cfg["rpm"]
entries.append(entry)
for name, cfg in matrix.get("optional_providers", {}).items():
entries.append({
"model_name": name,
"litellm_params": {
"model": cfg["model"],
"api_key": cfg.get("api_key", "os.environ/GROQ_API_KEY"),
},
})
litellm_rules = rules.get("litellm", {})
entries.append({
"model_name": "smart-router-internal",
"litellm_params": {
"model": "auto_router/complexity_router",
"drop_params": True,
"complexity_router_default_model": "a-medium-ops",
"complexity_router_config": {
"tiers": {
"SIMPLE": "a-simple",
"MEDIUM": "a-medium-ops",
"MEDIUM_CODE": "a-medium-code",
"COMPLEX": "a-complex",
"REASONING": "a-reasoning",
},
"classifier_fallback": "heuristic",
"keyword_tier_rules": litellm_rules.get("keyword_tier_rules", []),
"custom_technical_keywords": litellm_rules.get("custom_technical_keywords", []),
"token_thresholds": {"simple": 20, "complex": 500},
"tier_boundaries": {
"simple_medium": 0.18,
"medium_complex": 0.38,
"complex_reasoning": 0.62,
},
"session_affinity": True,
"session_affinity_ttl_seconds": 1800,
},
},
})
out = root / "litellm_config.generated.yaml"
out.write_text(
yaml.dump({"model_list": entries}, allow_unicode=True, sort_keys=False),
encoding="utf-8",
)
print(f"Wrote {out} ({len(entries)} models)")
PY
cd "$ROOT"
if [[ -f .env ]]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
python3 scripts/gen-litellm-config.py
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Extract first working vless:// URI from Happ/mireon subscription JSON."""
import json
import sys
import urllib.parse
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
configs = data if isinstance(data, list) else [data]
for j in configs:
if not isinstance(j, dict):
continue
for ob in j.get("outbounds", []):
if ob.get("protocol") != "vless":
continue
tag = ob.get("tag") or "proxy"
st = ob.get("settings") or {}
vnext = (st.get("vnext") or [{}])[0]
addr, port = vnext.get("address"), vnext.get("port")
users = (vnext.get("users") or [{}])[0]
uid = users.get("id")
if not all([addr, port, uid]) or addr in ("0.0.0.0", "127.0.0.1"):
continue
flow = users.get("flow") or ""
stream = ob.get("streamSettings") or {}
net = stream.get("network") or "tcp"
sec = stream.get("security") or "none"
rs = stream.get("realitySettings") or {}
ts = stream.get("tlsSettings") or {}
sni = rs.get("serverName") or ts.get("serverName") or ""
params = {"encryption": "none", "security": sec, "type": net}
if sni:
params["sni"] = sni
if rs.get("publicKey"):
params["pbk"] = rs["publicKey"]
if rs.get("shortId"):
params["sid"] = rs["shortId"]
if rs.get("fingerprint"):
params["fp"] = rs["fingerprint"]
if flow:
params["flow"] = flow
q = urllib.parse.urlencode(params)
print(f"vless://{uid}@{addr}:{port}?{q}#{tag}")
sys.exit(0)
sys.exit(1)
+3 -1
View File
@@ -10,7 +10,9 @@ set -a
source .env
set +a
NO_PROXY_VAL="${NO_PROXY:-localhost,127.0.0.1,api.novita.ai,novita.ai}"
# shellcheck disable=SC1091
source "${STACK_DIR}/scripts/no-proxy-default.sh"
NO_PROXY_VAL="${NO_PROXY:-${NO_PROXY_DEFAULT}}"
echo "== Scale vless-proxy and vpn-watchdog to 0 =="
docker service scale "${STACK_NAME}_vless-proxy=0" "${STACK_NAME}_vpn-watchdog=0" || true
+4 -2
View File
@@ -11,7 +11,9 @@ source .env
set +a
VLESS_URL="${VLESS_PROXY_URL:-http://vless-proxy:8080}"
NO_PROXY_VAL="${NO_PROXY:-localhost,127.0.0.1,api.novita.ai,novita.ai}"
# shellcheck disable=SC1091
source "${STACK_DIR}/scripts/no-proxy-default.sh"
NO_PROXY_VAL="${NO_PROXY:-${NO_PROXY_DEFAULT}}"
echo "== Scale vless-proxy and vpn-watchdog to 1 =="
docker service scale "${STACK_NAME}_vless-proxy=1" "${STACK_NAME}_vpn-watchdog=1"
@@ -42,4 +44,4 @@ docker service update \
--env-add "NO_PROXY=${NO_PROXY_VAL}" \
"${STACK_NAME}_litellm"
echo "VPN enabled. Novita bypasses via NO_PROXY."
echo "VPN enabled. Direct: Novita/Groq/Gemini/GigaChat. VPN: Anthropic/OpenRouter/Grok/Together."