feat(agent): hierarchical executor with path resolve, runtime probe, quiet UI
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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)"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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."
|
||||
|
||||
@@ -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)"
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user