2026-05-19 13:58:03 +08:00
#!/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/host_access_prep_lib.sh "
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 } "
2026-05-19 20:21:21 +08:00
CRM_HOST_BASE = " ${ CRM_HOST_BASE :- $HOST_BASE } "
2026-05-23 15:03:59 +08:00
REMOTE_HOST_BASE = " ${ REMOTE_HOST_BASE :- $CRM_HOST_BASE } "
2026-05-20 22:09:40 +08:00
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 } "
2026-05-23 15:03:59 +08:00
REMOTE_PG_CONTAINER = " ${ REMOTE_PG_CONTAINER :- } "
REMOTE_REDIS_CONTAINER = " ${ REMOTE_REDIS_CONTAINER :- } "
2026-05-21 09:18:17 +08:00
PACK_PATH = " ${ PACK_PATH :- $ROOT_DIR /packs/openai-cn-pack } "
2026-05-19 13:58:03 +08:00
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 } "
mkdir -p " $ART "
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
2026-05-21 21:19:19 +08:00
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
2026-05-19 13:58:03 +08:00
ssh_cmd( ) {
local cmd = " $1 "
ssh -i " $KEY " -o StrictHostKeyChecking = no " $REMOTE " " $cmd "
}
2026-05-23 15:03:59 +08:00
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 " ) "
2026-05-21 09:18:17 +08:00
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; "
}
2026-05-20 22:09:40 +08:00
crm_curl_json( ) {
local method = " $1 "
local path = " $2 "
local payload = " ${ 3 :- } "
if [ [ -n " $payload " ] ] ; then
curl -fsS -X " $method " \
-H " Authorization: Bearer $crm_token " \
-H 'Content-Type: application/json' \
" ${ CRM_BASE } ${ path } " \
-d " $payload "
else
curl -fsS -X " $method " \
-H " Authorization: Bearer $crm_token " \
" ${ CRM_BASE } ${ path } "
fi
}
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 } )
2026-05-23 15:03:59 +08:00
host_base = ${ REMOTE_HOST_BASE @Q }
2026-05-20 22:09:40 +08:00
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"
}
2026-05-19 13:58:03 +08:00
remote_pg_exec( ) {
local sql = " $1 "
local encoded
encoded = " $( printf '%s' " $sql " | base64 -w0) "
2026-05-20 22:09:40 +08:00
ssh_cmd " printf '%s' ' $encoded ' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api "
2026-05-19 13:58:03 +08:00
}
2026-05-19 20:21:21 +08:00
remote_pg_query( ) {
local sql = " $1 "
local encoded
encoded = " $( printf '%s' " $sql " | base64 -w0) "
2026-05-20 22:09:40 +08:00
ssh_cmd " printf '%s' ' $encoded ' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api -At -F $'\t' "
2026-05-19 20:21:21 +08:00
}
2026-05-19 13:58:03 +08:00
remote_fetch_group_state( ) {
local group_id = " $1 "
local user_id = " $2 "
local api_key = " $3 "
local output_path = " $4 "
2026-05-20 22:09:40 +08:00
local sql
sql = " $( python3 - " $group_id " " $user_id " " $api_key " <<'PY'
import sys
2026-05-19 13:58:03 +08:00
group_id, user_id, api_key = sys.argv[ 1:4]
2026-05-20 22:09:40 +08:00
api_key_literal = "'" + api_key.replace( "'" , "''" ) + "'"
2026-05-19 13:58:03 +08:00
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 (
2026-05-20 22:09:40 +08:00
SELECT row_to_json( k) AS data FROM api_keys k WHERE k.key = { api_key_literal}
2026-05-19 13:58:03 +08:00
)
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
) "
2026-05-20 22:09:40 +08:00
remote_pg_query " $sql " > " $output_path "
2026-05-19 13:58:03 +08:00
}
python3 - " $ART /00-local-key-source.json " " $key_source " " $provider_id " " $upstream_key " <<'PY'
import json, sys, pathlib
path, source, provider_id, key = sys.argv[ 1:5]
pathlib.Path( path) .write_text( json.dumps( {
'source' : source,
'provider_id' : provider_id,
'upstream_key_prefix' : key[ :12] ,
'upstream_key_suffix' : key[ -6:] ,
} , ensure_ascii = False, indent = 2) , encoding = 'utf-8' )
PY
2026-05-20 22:09:40 +08:00
crm_token = " ${ CRM_ADMIN_TOKEN :- } "
if [ [ -z " $crm_token " ] ] ; then
crm_token = " $( ssh_cmd "grep ^SUB2API_CRM_ADMIN_TOKEN= /home/ubuntu/sub2api-cn-relay-manager/.env.remote | cut -d= -f2-" ) "
crm_token = " ${ crm_token ##* $'\n' } "
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;\" " ) "
2026-05-19 13:58:03 +08:00
admin_uid = " ${ admin_uid ##* $'\n' } "
2026-05-19 20:21:21 +08:00
sub_uid = " $( remote_pg_query "select id from users where email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=users.id and s.deleted_at is null) order by id desc limit 1;" ) "
2026-05-19 13:58:03 +08:00
sub_uid = " ${ sub_uid ##* $'\n' } "
2026-05-19 20:21:21 +08:00
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;" ) "
2026-05-19 13:58:03 +08:00
sub_key = " ${ sub_key ##* $'\n' } "
2026-05-19 20:21:21 +08:00
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( "'" , "''" ) + "'"
2026-05-19 13:58:03 +08:00
2026-05-19 20:21:21 +08:00
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 <<EOF
$( remote_pg_query " $create_user_sql " )
EOF
fi
2026-05-23 15:03:59 +08:00
python3 - " $ART /01-runtime-context.json " " $CRM_BASE " " $HOST_BASE " " $CRM_HOST_BASE " " $REMOTE_HOST_BASE " " $provider_id " " $sub_uid " " $sub_key " <<'PY'
2026-05-19 13:58:03 +08:00
import json, sys, pathlib
2026-05-23 15:03:59 +08:00
path, crm, host, crm_host, remote_host, provider_id, sub_uid, sub_key = sys.argv[ 1:9]
2026-05-19 13:58:03 +08:00
pathlib.Path( path) .write_text( json.dumps( {
'crm_base' : crm,
'host_base' : host,
2026-05-19 20:21:21 +08:00
'crm_host_base' : crm_host,
2026-05-23 15:03:59 +08:00
'remote_host_base' : remote_host,
2026-05-19 13:58:03 +08:00
'provider_id' : provider_id,
'subscription_user_id' : sub_uid,
'subscription_user_key_prefix' : sub_key[ :12] ,
} , ensure_ascii = False, indent = 2) , encoding = 'utf-8' )
PY
2026-05-20 22:09:40 +08:00
create_host_payload = " $( python3 - " $HOST_NAME " " $CRM_HOST_BASE " " $host_bearer_token " <<'PY'
import json, sys
name, base_url, bearer_token = sys.argv[ 1:4]
print( json.dumps( {
'name' : name,
'base_url' : base_url,
'auth' : { 'type' : 'bearer' , 'token' : bearer_token} ,
} , ensure_ascii = False) )
PY
) "
hosts_payload = " $( crm_curl_json GET "/api/hosts" ) "
existing_host_json = " $( printf '%s' " $hosts_payload " | python3 -c ' import json, sys
base_url = sys.argv[ 1]
payload = json.load( sys.stdin)
for host in payload.get( "hosts" , [ ] ) :
if host.get( "base_url" ) = = base_url:
print( json.dumps( host, ensure_ascii = False) )
break' " $CRM_HOST_BASE " ) "
if [ [ -n " $existing_host_json " ] ] ; then
printf '%s\n' " $existing_host_json " > " $ART /01a-create-host.json "
else
crm_curl_json POST "/api/hosts" " $create_host_payload " > " $ART /01a-create-host.json "
fi
payload = " $( python3 - " $CRM_HOST_BASE " " $host_bearer_token " " $PACK_PATH " " $provider_id " " $upstream_key " " $sub_key " " $sub_uid " " $SUBSCRIPTION_DAYS " <<'PY'
2026-05-19 13:58:03 +08:00
import json, sys
2026-05-20 22:09:40 +08:00
host_base, host_bearer_token, pack_path, provider_id, upstream_key, sub_key, sub_uid, subscription_days = sys.argv[ 1:9]
2026-05-19 13:58:03 +08:00
print( json.dumps( {
'host_base_url' : host_base,
2026-05-20 22:09:40 +08:00
'host_bearer_token' : host_bearer_token,
2026-05-19 13:58:03 +08:00
'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
) "
2026-05-20 22:09:40 +08:00
curl -sS -D " $ART /02-import.headers.txt " -o " $ART /03-import.body.json " -X POST \
-H " Authorization: Bearer $crm_token " \
-H 'Content-Type: application/json' \
" $CRM_BASE /api/providers/ $provider_id /import " \
-d " $payload "
2026-05-19 13:58:03 +08:00
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
) "
2026-05-20 22:09:40 +08:00
crm_curl_json GET " /api/import-batches/ $batch_id " > " $ART /04-batch-detail-initial.json "
2026-05-19 20:21:21 +08:00
subscription_group_id = " $( python3 - " $ART /03-import.body.json " " $ART /04-batch-detail-initial.json " <<'PY'
2026-05-19 13:58:03 +08:00
import json, pathlib, sys
2026-05-19 20:21:21 +08:00
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' , [ ] ) :
2026-05-19 13:58:03 +08:00
if item.get( 'ResourceType' ) = = 'group' :
print( item.get( 'HostResourceID' , '' ) )
2026-05-19 20:21:21 +08:00
raise SystemExit( 0)
raise SystemExit( 'missing managed group in import response and batch detail' )
2026-05-19 13:58:03 +08:00
PY
) "
2026-05-21 09:18:17 +08:00
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' } "
2026-05-19 20:21:21 +08:00
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 " ) "
2026-05-19 13:58:03 +08:00
prep_sql = " $( build_subscription_access_prep_sql " $sub_uid " " $sub_key " " $subscription_group_id " " $MIN_BALANCE " " $SUBSCRIPTION_DAYS " " $admin_uid " " $SUBSCRIPTION_NOTES " ) "
python3 - " $ART /05-subscription-access-prep.sql " " $prep_sql " <<'PY'
import pathlib, sys
pathlib.Path( sys.argv[ 1] ) .write_text( sys.argv[ 2] , encoding = 'utf-8' )
PY
remote_pg_exec " $prep_sql " > " $ART /06-subscription-access-prep.psql.txt "
2026-05-19 20:21:21 +08:00
{
printf 'auth_cache_key=%s\n' " $auth_cache_key "
printf 'balance_cache_key=%s\n' " $balance_cache_key "
printf 'subscription_cache_key=%s\n' " $subscription_cache_key "
2026-05-20 22:09:40 +08:00
ssh_cmd " sudo -n docker exec $REMOTE_REDIS_CONTAINER_Q redis-cli DEL $auth_cache_key $balance_cache_key $subscription_cache_key "
2026-05-19 20:21:21 +08:00
} > " $ART /07-redis-targeted-invalidation.txt "
2026-05-21 09:18:17 +08:00
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
2026-05-19 13:58:03 +08:00
2026-05-23 15:03:59 +08:00
python3 - " $ART /01-runtime-context.json " " $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 " <<'PY'
2026-05-19 13:58:03 +08:00
import json, sys, pathlib
2026-05-23 15:03:59 +08:00
path, 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 = sys.argv[ 1:14]
2026-05-19 13:58:03 +08:00
pathlib.Path( path) .write_text( json.dumps( {
'crm_base' : crm,
'host_base' : host,
2026-05-19 20:21:21 +08:00
'crm_host_base' : crm_host,
2026-05-23 15:03:59 +08:00
'remote_host_base' : remote_host,
2026-05-19 13:58:03 +08:00
'provider_id' : provider_id,
'subscription_user_id' : sub_uid,
'subscription_user_key_prefix' : sub_key[ :12] ,
'subscription_group_id' : group_id,
'admin_user_id' : admin_uid,
2026-05-21 09:18:17 +08:00
'managed_user_email' : managed_user_email,
'managed_user_id' : managed_user_id,
'managed_probe_key_prefix' : managed_probe_key[ :18] ,
2026-05-19 13:58:03 +08:00
} , ensure_ascii = False, indent = 2) , encoding = 'utf-8' )
PY
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
) "
2026-05-23 15:03:59 +08:00
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 "
2026-05-19 20:21:21 +08:00
ssh_cmd "cat /tmp/models_headers.txt" > " $ART /09-models.headers.txt "
ssh_cmd "cat /tmp/models_body.json" > " $ART /10-models.body.json "
2026-05-23 15:03:59 +08:00
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 " ) "
2026-05-19 20:21:21 +08:00
ssh_cmd "cat /tmp/chat_headers.txt" > " $ART /11-chat.headers.txt "
ssh_cmd "cat /tmp/chat_body.json" > " $ART /12-chat.body.json "
2026-05-19 13:58:03 +08:00
2026-05-21 21:19:19 +08:00
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 "
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 "
2026-05-23 15:03:59 +08:00
provider_query_suffix = " ?host_id= $( python3 - " $HOST_NAME " <<'PY'
import sys
from urllib.parse import quote
print( quote( sys.argv[ 1] , 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 "
2026-05-19 13:58:03 +08:00
preview_payload = " $( python3 - " $provider_id " <<'PY'
import json, sys
print( json.dumps( { 'provider_id' : sys.argv[ 1] , 'mode' : 'subscription' } , ensure_ascii = False) )
PY
) "
2026-05-23 15:03:59 +08:00
crm_curl_json POST " /api/providers/ $provider_id /access/preview ${ provider_query_suffix } " " $preview_payload " > " $ART /15-access-preview.json "
2026-05-20 22:09:40 +08:00
crm_curl_json GET " /api/import-batches/ $batch_id " > " $ART /16-batch-detail-final.json "
2026-05-19 13:58:03 +08:00
2026-05-19 20:21:21 +08:00
python3 - " $ART " " $provider_id " " $batch_id " " $subscription_group_id " " $model_name " <<'PY'
2026-05-19 13:58:03 +08:00
import json, pathlib, sys
2026-05-20 22:09:40 +08:00
2026-05-19 13:58:03 +08:00
art = pathlib.Path( sys.argv[ 1] )
provider_id = sys.argv[ 2]
batch_id = int( sys.argv[ 3] )
2026-05-19 20:21:21 +08:00
subscription_group_id = sys.argv[ 4]
expected_model = sys.argv[ 5]
2026-05-21 21:19:19 +08:00
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 { }
2026-05-19 13:58:03 +08:00
import_obj = json.loads( ( art/'03-import.body.json' ) .read_text( ) )
2026-05-21 21:19:19 +08:00
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' )
2026-05-19 20:21:21 +08:00
models_headers = ( art/'09-models.headers.txt' ) .read_text( )
chat_headers = ( art/'11-chat.headers.txt' ) .read_text( )
2026-05-21 21:19:19 +08:00
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' )
2026-05-19 20:21:21 +08:00
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)
2026-05-21 21:19:19 +08:00
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'
2026-05-19 13:58:03 +08:00
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' ) ,
2026-05-19 20:21:21 +08:00
'direct_models_http200' : '200 OK' in models_headers,
2026-05-21 21:19:19 +08:00
'direct_models_has_expected_model' : direct_has_expected_model,
2026-05-19 20:21:21 +08:00
'direct_models' : models,
2026-05-19 13:58:03 +08:00
'direct_chat_http200' : '200 OK' in chat_headers,
2026-05-21 21:19:19 +08:00
'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,
2026-05-19 13:58:03 +08:00
'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' ) ,
2026-05-19 20:21:21 +08:00
'subscription_group_id' : subscription_group_id,
'import_group_id' : ( import_obj.get( 'group' ) or { } ) .get( 'id' ) ,
2026-05-19 13:58:03 +08:00
}
2026-05-21 21:19:19 +08:00
summary_json = json.dumps( summary, ensure_ascii = False)
( art / '21-summary.json' ) .write_text( summary_json, encoding = 'utf-8' )
print( summary_json)
2026-05-19 13:58:03 +08:00
PY