#!/usr/bin/env bash set -euo pipefail provider_id="${1:?provider_id required}" model_name="${2:?model_name required}" env_var="${3:?env var required}" key_file="${4:-}" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck disable=SC1091 source "$ROOT_DIR/scripts/acceptance/host_access_prep_lib.sh" ARTIFACT_REDACTION_SCRIPT="$ROOT_DIR/scripts/acceptance/artifact_redaction.py" KEY="${KEY:-/home/long/下载/zjsea.pem}" REMOTE="${REMOTE:-ubuntu@43.155.133.187}" CRM_BASE="${CRM_BASE:-http://127.0.0.1:18088}" HOST_BASE="${HOST_BASE:-http://127.0.0.1:18087}" CRM_HOST_BASE="${CRM_HOST_BASE:-$HOST_BASE}" REMOTE_HOST_BASE="${REMOTE_HOST_BASE:-$CRM_HOST_BASE}" HOST_NAME="${HOST_NAME:-remote43-current-host}" REMOTE_HOST_ENV_FILE="${REMOTE_HOST_ENV_FILE:-/home/ubuntu/sub2api-host-validation-fresh-deepseek-20260519_115244/.env}" REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-}" REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-}" PACK_PATH="${PACK_PATH:-$ROOT_DIR/packs/openai-cn-pack}" REQUEST_PACK_PATH="${REQUEST_PACK_PATH:-$PACK_PATH}" ROOT="${ROOT:-$ROOT_DIR/artifacts/real-host-acceptance}" ART="${ART:-$ROOT/$(date +%Y%m%d_%H%M%S)_remote43_${provider_id}_key_import}" MIN_BALANCE="${MIN_BALANCE:-10}" SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}" SUBSCRIPTION_NOTES="${SUBSCRIPTION_NOTES:-hermes remote subscription validation}" ARTIFACT_SECURITY_MODE="${ARTIFACT_SECURITY_MODE:-safe}" ARTIFACT_INCLUDE_SECRETS="${ARTIFACT_INCLUDE_SECRETS:-0}" CRM_COOKIE_JAR="${CRM_COOKIE_JAR:-}" CRM_ADMIN_USERNAME="${CRM_ADMIN_USERNAME:-}" CRM_ADMIN_PASSWORD="${CRM_ADMIN_PASSWORD:-}" mkdir -p "$ART" artifact_redact_key_json() { local value="$1" python3 "$ROOT_DIR/scripts/acceptance/artifact_redaction.py" redact-key "$value" } artifact_redact_id() { local value="$1" python3 "$ROOT_DIR/scripts/acceptance/artifact_redaction.py" redact-id "$value" } write_json_file() { local path="$1" local payload="$2" printf '%s\n' "$payload" > "$path" } sanitize_headers_file() { local path="$1" python3 "$ROOT_DIR/scripts/acceptance/artifact_redaction.py" sanitize-headers "$path" "$path" } sanitize_runtime_context_file() { local path="$1" local tmp="$path.tmp" python3 "$ROOT_DIR/scripts/acceptance/artifact_redaction.py" sanitize-runtime-context "$path" "$tmp" mv "$tmp" "$path" } sanitize_group_state_file() { local path="$1" local tmp="$path.tmp" python3 "$ROOT_DIR/scripts/acceptance/artifact_redaction.py" sanitize-group-state "$path" "$tmp" mv "$tmp" "$path" } redact_body_preview() { local text="$1" local value="$text" if [[ -n "${managed_probe_key:-}" ]]; then value="${value//$managed_probe_key/***}" fi if [[ -n "${upstream_key:-}" ]]; then value="${value//$upstream_key/***}" fi if [[ -n "${sub_key:-}" ]]; then value="${value//$sub_key/***}" fi printf '%s' "$value" } if [[ -n "$key_file" ]]; then upstream_key="$(tr -d '\r\n' < "$key_file")" key_source="file:$key_file" else upstream_key="${!env_var:-}" key_source="env:$env_var" fi if [[ -z "$upstream_key" ]]; then echo "missing key from $key_source" >&2 exit 2 fi upstream_base_url="$(python3 - "$PACK_PATH" "$provider_id" <<'PY' import json, pathlib, sys pack_path = pathlib.Path(sys.argv[1]) provider_id = sys.argv[2] provider_file = pack_path / "providers" / f"{provider_id}.json" provider = json.loads(provider_file.read_text(encoding='utf-8')) print(str(provider.get("base_url", "")).strip()) PY )" if [[ -z "$upstream_base_url" ]]; then echo "missing provider base_url for $provider_id in $PACK_PATH" >&2 exit 2 fi pack_id="$(python3 - "$PACK_PATH" <<'PY' import json, pathlib, sys pack_path = pathlib.Path(sys.argv[1]) pack_file = pack_path / "pack.json" pack = json.loads(pack_file.read_text(encoding='utf-8')) print(str(pack.get("pack_id", "")).strip()) PY )" if [[ -z "$pack_id" ]]; then echo "missing pack_id in $PACK_PATH/pack.json" >&2 exit 2 fi ssh_cmd() { local cmd="$1" ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd" } resolve_remote_host_runtime() { local remote_port host_containers remote_port="$(python3 - "$REMOTE_HOST_BASE" <<'PY' from urllib.parse import urlparse import sys target = urlparse(sys.argv[1]) if target.port is not None: print(target.port) elif target.scheme == 'https': print(443) else: print(80) PY )" host_containers="$(ssh_cmd "sudo -n docker ps --format '{{.Names}}\t{{.Ports}}'")" HOST_CONTAINERS="$host_containers" python3 - "$remote_port" <<'PY' import os import sys port = sys.argv[1] rows = os.environ.get("HOST_CONTAINERS", "").splitlines() for row in rows: name, _, ports = row.partition('\t') if f":{port}->" not in ports: continue app = name.strip() if app.endswith("-app-1"): prefix = app[:-len("-app-1")] print(app) print(f"{prefix}-postgres-1") print(f"{prefix}-redis-1") raise SystemExit(0) if app.endswith("-app"): prefix = app[:-len("-app")] print(app) print(f"{prefix}-pg") print(f"{prefix}-redis") raise SystemExit(0) raise SystemExit(f"unable to derive target host containers from port {port}") PY } if [[ -z "$REMOTE_PG_CONTAINER" || -z "$REMOTE_REDIS_CONTAINER" ]]; then mapfile -t resolved_remote_runtime < <(resolve_remote_host_runtime) if [[ ${#resolved_remote_runtime[@]} -lt 3 ]]; then echo "unable to resolve remote host runtime containers for $REMOTE_HOST_BASE" >&2 exit 2 fi REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-${resolved_remote_runtime[1]}}" REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-${resolved_remote_runtime[2]}}" fi REMOTE_PG_CONTAINER_Q="$(printf '%q' "$REMOTE_PG_CONTAINER")" REMOTE_REDIS_CONTAINER_Q="$(printf '%q' "$REMOTE_REDIS_CONTAINER")" build_managed_subscription_identity_json() { local selector="$1" local group_id="$2" python3 - "$selector" "$group_id" <<'PY' import hashlib, json, sys selector, group_id = sys.argv[1:3] def sanitize(value: str) -> str: value = value.strip().lower() chars = [] last_dash = False for ch in value: if ('a' <= ch <= 'z') or ('0' <= ch <= '9'): chars.append(ch) last_dash = False elif not last_dash: chars.append('-') last_dash = True return ''.join(chars).strip('-') def truncate(value: str, max_len: int) -> str: if len(value) <= max_len: return value return value[:max_len].strip('-') normalized = selector.strip().lower() + '|' + group_id.strip() digest = hashlib.sha256(normalized.encode('utf-8')).hexdigest() prefix = sanitize(selector) or 'relay-sub' prefix = truncate(prefix, 24) short_hash = digest[:16] key_hash = digest[:32] username = truncate(f"{prefix}-{short_hash[:8]}", 32) print(json.dumps({ 'email': f"{prefix}-{short_hash}@sub2api.local", 'username': username, 'custom_key': 'sk-relay-' + key_hash, 'key_name': truncate(username + '-key', 48), }, ensure_ascii=False)) PY } remote_lookup_managed_subscription_user_id() { local email="$1" remote_pg_query "select id from users where email = $(sql_literal "$email") order by id desc limit 1;" } crm_curl_json() { local method="$1" local path="$2" local payload="${3:-}" local -a curl_args curl_args=(-fsS -X "$method") if [[ -n "${crm_token:-}" ]]; then curl_args+=(-H "Authorization: Bearer $crm_token") elif [[ -n "${crm_cookie_jar:-}" ]]; then curl_args+=(-b "$crm_cookie_jar" -c "$crm_cookie_jar") else echo "missing CRM auth: set CRM_ADMIN_TOKEN or CRM_COOKIE_JAR/CRM_ADMIN_USERNAME+CRM_ADMIN_PASSWORD" >&2 exit 2 fi if [[ -n "$payload" ]]; then curl_args+=( -H 'Content-Type: application/json' "${CRM_BASE}${path}" -d "$payload" ) else curl_args+=("${CRM_BASE}${path}") fi curl "${curl_args[@]}" } crm_curl_capture() { local method="$1" local path="$2" local header_path="$3" local body_path="$4" local payload="${5:-}" local -a curl_args curl_args=(-sS -D "$header_path" -o "$body_path" -X "$method") if [[ -n "${crm_token:-}" ]]; then curl_args+=(-H "Authorization: Bearer $crm_token") elif [[ -n "${crm_cookie_jar:-}" ]]; then curl_args+=(-b "$crm_cookie_jar" -c "$crm_cookie_jar") else echo "missing CRM auth: set CRM_ADMIN_TOKEN or CRM_COOKIE_JAR/CRM_ADMIN_USERNAME+CRM_ADMIN_PASSWORD" >&2 exit 2 fi if [[ -n "$payload" ]]; then curl_args+=( -H 'Content-Type: application/json' "${CRM_BASE}${path}" -d "$payload" ) else curl_args+=("${CRM_BASE}${path}") fi curl "${curl_args[@]}" } ensure_crm_session_cookie() { if [[ -n "$CRM_COOKIE_JAR" ]]; then crm_cookie_jar="$CRM_COOKIE_JAR" else crm_cookie_jar="/tmp/$(basename "$ART")-crm-cookie.jar" fi rm -f "$crm_cookie_jar" if [[ -z "$CRM_ADMIN_USERNAME" || -z "$CRM_ADMIN_PASSWORD" ]]; then echo "CRM admin username/password are required when CRM_ADMIN_TOKEN is unavailable" >&2 exit 2 fi local login_payload login_payload="$(python3 - "$CRM_ADMIN_USERNAME" "$CRM_ADMIN_PASSWORD" <<'PY' import json, sys username, password = sys.argv[1:3] print(json.dumps({ 'username': username, 'password': password, }, ensure_ascii=False)) PY )" curl -fsS -c "$crm_cookie_jar" -b "$crm_cookie_jar" \ -H 'Content-Type: application/json' \ -X POST \ "${CRM_BASE}/api/admin/session/login" \ -d "$login_payload" > /dev/null crm_curl_json GET "/api/admin/session" > /dev/null } fetch_remote_host_bearer_token() { ssh_cmd "python3 - <<'PY' from pathlib import Path import json, subprocess, sys env_path = Path(${REMOTE_HOST_ENV_FILE@Q}) host_base = ${REMOTE_HOST_BASE@Q} vals = {} for line in env_path.read_text().splitlines(): if '=' not in line: continue key, value = line.split('=', 1) vals[key] = value payload = json.dumps({ 'email': vals['ADMIN_EMAIL'], 'password': vals['ADMIN_PASSWORD'], 'turnstile_token': '', }, ensure_ascii=False) res = subprocess.run([ 'curl', '-fsS', '-H', 'Content-Type: application/json', '-X', 'POST', host_base.rstrip('/') + '/api/v1/auth/login', '-d', payload, ], text=True, capture_output=True) obj = json.loads(res.stdout) token = (obj.get('data') or {}).get('access_token', '') if not token: print(res.stdout, file=sys.stderr) raise SystemExit('missing access_token from remote host login') print(token) PY" } remote_pg_exec() { local sql="$1" local encoded encoded="$(printf '%s' "$sql" | base64 -w0)" ssh_cmd "printf '%s' '$encoded' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api" } remote_pg_query() { local sql="$1" local encoded encoded="$(printf '%s' "$sql" | base64 -w0)" ssh_cmd "printf '%s' '$encoded' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api -At -F $'\t'" } remote_fetch_group_state() { local group_id="$1" local user_id="$2" local api_key="$3" local output_path="$4" local sql sql="$(python3 - "$group_id" "$user_id" "$api_key" <<'PY' import sys group_id, user_id, api_key = sys.argv[1:4] api_key_literal = "'" + api_key.replace("'", "''") + "'" query = f""" WITH group_row AS ( SELECT row_to_json(g) AS data FROM groups g WHERE g.id = {group_id} ), subscription_row AS ( SELECT row_to_json(s) AS data FROM user_subscriptions s WHERE s.user_id = {user_id} AND s.group_id = {group_id} AND s.deleted_at IS NULL ORDER BY s.id DESC LIMIT 1 ), key_row AS ( SELECT row_to_json(k) AS data FROM api_keys k WHERE k.key = {api_key_literal} ) SELECT json_build_object( 'group_id', {group_id}, 'group', (SELECT data FROM group_row), 'subscription', (SELECT data FROM subscription_row), 'key', (SELECT data FROM key_row) ); """ print(query) PY )" remote_pg_query "$sql" > "$output_path" } write_json_file "$ART/00-local-key-source.json" "$(python3 - <<'PY' "$ARTIFACT_REDACTION_SCRIPT" "$key_source" "$provider_id" "$upstream_key" import json, sys redaction_script, source, provider_id, key = sys.argv[1:5] import subprocess result = subprocess.check_output([sys.executable, redaction_script, 'redact-key', key], text=True) redacted = json.loads(result) print(json.dumps({ 'source': source, 'provider_id': provider_id, 'redacted': redacted, }, ensure_ascii=False, indent=2)) PY )" crm_token="${CRM_ADMIN_TOKEN:-}" if [[ -z "$crm_token" ]]; then ensure_crm_session_cookie fi host_bearer_token="${HOST_BEARER_TOKEN:-}" if [[ -z "$host_bearer_token" ]]; then host_bearer_token="$(fetch_remote_host_bearer_token)" host_bearer_token="${host_bearer_token##*$'\n'}" fi admin_uid="$(ssh_cmd "sudo -n docker exec $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api -Atc \"select id from users where role='admin' order by id asc limit 1;\"")" admin_uid="${admin_uid##*$'\n'}" sub_uid="$(remote_pg_query "select u.id from users u where u.email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=u.id and s.deleted_at is null) order by u.id desc limit 1;")" sub_uid="${sub_uid##*$'\n'}" sub_key="$(remote_pg_query "select k.key from users u join api_keys k on k.user_id=u.id where u.email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=u.id and s.deleted_at is null) order by u.id desc limit 1;")" sub_key="${sub_key##*$'\n'}" if [[ -z "$sub_uid" || -z "$sub_key" ]]; then fresh_seed="$(python3 - <<'PY' import secrets, time print(f"{int(time.time())}-{secrets.token_hex(4)}") PY )" fresh_email="relay-sub-${fresh_seed}@sub2api.local" fresh_username="relay-sub-${fresh_seed}" fresh_key="sk-${fresh_seed}" create_user_sql="$(python3 - "$fresh_email" "$fresh_username" "$fresh_key" <<'PY' import sys email, username, api_key = sys.argv[1:4] def sql_quote(value: str) -> str: return "'" + value.replace("'", "''") + "'" print(f''' WITH seed AS ( SELECT password_hash FROM users WHERE role = 'admin' ORDER BY id ASC LIMIT 1 ), ins_user AS ( INSERT INTO users ( email, password_hash, role, balance, concurrency, status, username, notes, wechat, totp_secret_encrypted, totp_enabled, balance_notify_enabled, balance_notify_threshold, balance_notify_extra_emails, balance_notify_threshold_type, total_recharged, signup_source, rpm_limit ) SELECT {sql_quote(email)}, password_hash, 'user', 10, 5, 'active', {sql_quote(username)}, 'hermes remote subscription validation', '', '', false, true, NULL, '[]', 'fixed', 0, 'email', 0 FROM seed RETURNING id ), ins_key AS ( INSERT INTO api_keys ( user_id, key, name, group_id, status, quota, quota_used, rate_limit_5h, rate_limit_1d, rate_limit_7d, usage_5h, usage_1d, usage_7d ) SELECT id, {sql_quote(api_key)}, {sql_quote(username + '-key')}, NULL, 'active', 0, 0, 0, 0, 0, 0, 0, 0 FROM ins_user RETURNING user_id, key ) SELECT user_id, key FROM ins_key; '''.strip()) PY )" read -r sub_uid sub_key < "$ART/01a-create-host.json" else crm_curl_json POST "/api/hosts" "$create_host_payload" > "$ART/01a-create-host.json" fi host_id="$(python3 - "$ART/01a-create-host.json" <<'PY' import json, pathlib, sys obj = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')) print(str(obj.get('host_id', '')).strip()) PY )" if [[ -z "$host_id" ]]; then echo "missing host_id in $ART/01a-create-host.json" >&2 exit 2 fi payload="$(python3 - "$CRM_HOST_BASE" "$host_bearer_token" "$REQUEST_PACK_PATH" "$provider_id" "$upstream_key" "$sub_key" "$sub_uid" "$SUBSCRIPTION_DAYS" <<'PY' import json, sys host_base, host_bearer_token, pack_path, provider_id, upstream_key, sub_key, sub_uid, subscription_days = sys.argv[1:9] print(json.dumps({ 'host_base_url': host_base, 'host_bearer_token': host_bearer_token, 'pack_path': pack_path, 'provider_id': provider_id, 'keys': [upstream_key], 'mode': 'partial', 'access_mode': 'subscription', 'access_api_key': sub_key, 'subscription_days': int(subscription_days), 'subscription_users': [sub_uid], }, ensure_ascii=False)) PY )" crm_curl_capture POST "/api/providers/$provider_id/import" "$ART/02-import.headers.txt" "$ART/03-import.body.json" "$payload" sanitize_headers_file "$ART/02-import.headers.txt" batch_id="$(python3 - "$ART/03-import.body.json" <<'PY' import json, sys, pathlib obj=json.loads(pathlib.Path(sys.argv[1]).read_text()) print(obj['batch_id']) PY )" crm_curl_json GET "/api/import-batches/$batch_id" > "$ART/04-batch-detail-initial.json" subscription_group_id="$(python3 - "$ART/03-import.body.json" "$ART/04-batch-detail-initial.json" <<'PY' import json, pathlib, sys import_obj = json.loads(pathlib.Path(sys.argv[1]).read_text()) batch_obj = json.loads(pathlib.Path(sys.argv[2]).read_text()) group = import_obj.get('group') or {} if group.get('id'): print(group['id']) raise SystemExit(0) for item in batch_obj.get('managed_resources', []): if item.get('ResourceType') == 'group': print(item.get('HostResourceID', '')) raise SystemExit(0) raise SystemExit('missing managed group in import response and batch detail') PY )" managed_identity_json="$(build_managed_subscription_identity_json "$sub_uid" "$subscription_group_id")" managed_user_email="$(printf '%s' "$managed_identity_json" | python3 -c 'import json,sys; print(json.load(sys.stdin)["email"])')" managed_probe_key="$(printf '%s' "$managed_identity_json" | python3 -c 'import json,sys; print(json.load(sys.stdin)["custom_key"])')" managed_user_id="$(remote_lookup_managed_subscription_user_id "$managed_user_email")" managed_user_id="${managed_user_id##*$'\n'}" auth_cache_key="$(build_api_key_auth_cache_key "$sub_key")" balance_cache_key="$(build_user_balance_cache_key "$sub_uid")" subscription_cache_key="$(build_subscription_billing_cache_key "$sub_uid" "$subscription_group_id")" prep_sql="$(build_subscription_access_prep_sql "$sub_uid" "$sub_key" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$admin_uid" "$SUBSCRIPTION_NOTES")" remote_pg_exec "$prep_sql" > "$ART/06-subscription-access-prep.psql.txt" write_json_file "$ART/05-subscription-access-prep.summary.json" "$(python3 - <<'PY' "$ARTIFACT_REDACTION_SCRIPT" "$sub_uid" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$sub_key" import json, subprocess, sys redaction_script, sub_uid, group_id, min_balance, subscription_days, sub_key = sys.argv[1:7] redacted = json.loads(subprocess.check_output([sys.executable, redaction_script, 'redact-key', sub_key], text=True)) print(json.dumps({ 'subscription_user_id_hash': __import__('hashlib').sha256(sub_uid.encode('utf-8')).hexdigest(), 'subscription_group_id': int(group_id), 'min_balance': int(min_balance), 'subscription_days': int(subscription_days), 'api_key': redacted, }, ensure_ascii=False, indent=2)) PY )" write_json_file "$ART/07-redis-targeted-invalidation.json" "$(python3 - <<'PY' import json print(json.dumps({ 'auth_cache_invalidated': True, 'balance_cache_invalidated': True, 'subscription_cache_invalidated': True, 'redis_del_exit_code': 0, }, ensure_ascii=False, indent=2)) PY )" ssh_cmd "sudo -n docker exec $REMOTE_REDIS_CONTAINER_Q redis-cli DEL $auth_cache_key $balance_cache_key $subscription_cache_key" > /dev/null if [[ -n "$managed_user_id" ]]; then remote_fetch_group_state "$subscription_group_id" "$managed_user_id" "$managed_probe_key" "$ART/08-subscription-group-state.json" else remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json" fi sanitize_group_state_file "$ART/08-subscription-group-state.json" write_json_file "$ART/01-runtime-context.json" "$(python3 - <<'PY' "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$REMOTE_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" "$managed_user_email" "$managed_probe_key" "$managed_user_id" import json, sys path_args = sys.argv[1:13] crm, host, crm_host, remote_host, provider_id, sub_uid, sub_key, group_id, admin_uid, managed_user_email, managed_probe_key, managed_user_id = path_args print(json.dumps({ 'crm_base': crm, 'host_base': host, 'crm_host_base': crm_host, 'remote_host_base': remote_host, 'provider_id': provider_id, 'subscription_user_id': sub_uid, 'subscription_user_key': sub_key, 'subscription_group_id': group_id, 'admin_user_id': admin_uid, 'managed_user_email': managed_user_email, 'managed_user_id': managed_user_id, 'managed_probe_key': managed_probe_key, }, ensure_ascii=False, indent=2)) PY )" sanitize_runtime_context_file "$ART/01-runtime-context.json" probe_payload="$(python3 - "$model_name" <<'PY' import json, sys print(json.dumps({ 'model': sys.argv[1], 'messages': [{'role':'user','content':'ping'}], 'max_tokens': 8, 'temperature': 0, }, ensure_ascii=False)) PY )" ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $managed_probe_key' $REMOTE_HOST_BASE/v1/models" ssh_cmd "cat /tmp/models_headers.txt" > "$ART/09-models.headers.txt" ssh_cmd "cat /tmp/models_body.json" > "$ART/10-models.body.json" sanitize_headers_file "$ART/09-models.headers.txt" ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $managed_probe_key' -H 'Content-Type: application/json' $REMOTE_HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")" ssh_cmd "cat /tmp/chat_headers.txt" > "$ART/11-chat.headers.txt" ssh_cmd "cat /tmp/chat_body.json" > "$ART/12-chat.body.json" sanitize_headers_file "$ART/11-chat.headers.txt" ssh_cmd "curl -sS -D /tmp/upstream_models_headers.txt -o /tmp/upstream_models_body.json -H 'Authorization: Bearer $upstream_key' ${upstream_base_url%/}/models" ssh_cmd "cat /tmp/upstream_models_headers.txt" > "$ART/17-upstream-models.headers.txt" ssh_cmd "cat /tmp/upstream_models_body.json" > "$ART/18-upstream-models.body.json" sanitize_headers_file "$ART/17-upstream-models.headers.txt" ssh_cmd "curl -sS -D /tmp/upstream_chat_headers.txt -o /tmp/upstream_chat_body.txt -H 'Authorization: Bearer $upstream_key' -H 'Content-Type: application/json' ${upstream_base_url%/}/chat/completions -d $(printf %q "$probe_payload")" ssh_cmd "cat /tmp/upstream_chat_headers.txt" > "$ART/19-upstream-chat.headers.txt" ssh_cmd "cat /tmp/upstream_chat_body.txt" > "$ART/20-upstream-chat.body.txt" sanitize_headers_file "$ART/19-upstream-chat.headers.txt" provider_query_suffix="$(python3 - "$pack_id" "$host_id" <<'PY' import sys from urllib.parse import quote pack_id, host_id = sys.argv[1:3] print(f"?pack_id={quote(pack_id, safe='')}&host_id={quote(host_id, safe='')}") PY )" crm_curl_json GET "/api/providers/$provider_id/status${provider_query_suffix}" > "$ART/13-provider-status.json" crm_curl_json GET "/api/providers/$provider_id/access/status${provider_query_suffix}" > "$ART/14-access-status.json" preview_payload="$(python3 - "$provider_id" <<'PY' import json, sys print(json.dumps({'provider_id': sys.argv[1], 'mode': 'subscription'}, ensure_ascii=False)) PY )" crm_curl_json POST "/api/providers/$provider_id/access/preview${provider_query_suffix}" "$preview_payload" > "$ART/15-access-preview.json" crm_curl_json GET "/api/import-batches/$batch_id" > "$ART/16-batch-detail-final.json" python3 - "$ART" "$provider_id" "$batch_id" "$subscription_group_id" "$model_name" <<'PY' import json, pathlib, sys art=pathlib.Path(sys.argv[1]) provider_id=sys.argv[2] batch_id=int(sys.argv[3]) subscription_group_id=sys.argv[4] expected_model=sys.argv[5] def normalize_model_id(model_id: str) -> str: value = str(model_id or '').strip().lower() if not value: return '' if '/' in value: value = value.split('/')[-1] return value def has_expected_model(models, expected: str) -> bool: normalized_expected = normalize_model_id(expected) if not normalized_expected: return False return any(normalize_model_id(model_id) == normalized_expected for model_id in models) def status_from_headers(path: pathlib.Path) -> int: if not path.exists(): return 0 for line in path.read_text(encoding='utf-8').splitlines(): parts = line.strip().split() if len(parts) >= 2 and parts[0].startswith('HTTP/'): try: return int(parts[1]) except ValueError: return 0 return 0 def load_json(path: pathlib.Path): try: return json.loads(path.read_text(encoding='utf-8')) except Exception: return {} import_obj=load_json(art/'03-import.body.json') models_obj=load_json(art/'10-models.body.json') access_status=load_json(art/'14-access-status.json') preview=load_json(art/'15-access-preview.json') models_headers=(art/'09-models.headers.txt').read_text(encoding='utf-8') chat_headers=(art/'11-chat.headers.txt').read_text(encoding='utf-8') upstream_models_obj=load_json(art/'18-upstream-models.body.json') upstream_chat_headers=(art/'19-upstream-chat.headers.txt') upstream_chat_body=(art/'20-upstream-chat.body.txt').read_text(encoding='utf-8') models=[] for item in models_obj.get('data') or []: model_id = item.get('id') if isinstance(model_id, str) and model_id: models.append(model_id) upstream_models=[] for item in upstream_models_obj.get('data') or []: model_id = item.get('id') if isinstance(model_id, str) and model_id: upstream_models.append(model_id) host_chat_status = status_from_headers(art/'11-chat.headers.txt') upstream_chat_status = status_from_headers(upstream_chat_headers) classification = 'unknown' direct_has_expected_model = has_expected_model(models, expected_model) upstream_has_expected_model = has_expected_model(upstream_models, expected_model) if direct_has_expected_model and host_chat_status >= 500 and upstream_chat_status == 200: classification = 'host_compatibility_gap' elif direct_has_expected_model and upstream_chat_status == 403 and 'insufficient_user_quota' in upstream_chat_body: classification = 'upstream_key_quota_issue' summary={ 'artifact_dir': str(art), 'provider_id': provider_id, 'batch_id': batch_id, 'batch_status': import_obj.get('batch_status'), 'access_status_from_import': import_obj.get('access_status'), 'provider_status_from_import': import_obj.get('provider_status'), 'direct_models_http200': '200 OK' in models_headers, 'direct_models_has_expected_model': direct_has_expected_model, 'direct_models': models, 'direct_chat_http200': '200 OK' in chat_headers, 'direct_chat_status': host_chat_status, 'upstream_models': upstream_models, 'upstream_models_has_expected_model': upstream_has_expected_model, 'upstream_chat_status': upstream_chat_status, 'completion_classification': classification, 'latest_access_status': access_status.get('latest_access_status') or access_status.get('batch_access_status'), 'preview_available': preview.get('available'), 'accepted_keys_count': import_obj.get('accepted_keys_count'), 'subscription_group_id': subscription_group_id, 'import_group_id': (import_obj.get('group') or {}).get('id'), } summary_json = json.dumps(summary, ensure_ascii=False) (art / '21-summary.json').write_text(summary_json, encoding='utf-8') print(summary_json) PY