"""ModelHub XC automatic model adaptation task agent. The worker discovers a public text-generation model, checks whether the same model/card pair already has a task, and submits at most one verified adaptation task per scan. Credentials are read only from the runtime environment. """ from __future__ import annotations import json import os import re import signal import threading from datetime import datetime, timezone from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any, Callable, Mapping from urllib.parse import quote, urlencode, urlsplit from urllib.request import Request, build_opener AGENT_NAME = "xc-model-auto-adaptation-agent" AGENT_VERSION = "1.0.1" TARGET_VENDOR = "天数智芯" TARGET_CARD = "天垓100" DEFAULT_TARGET_GPU = "Iluvatar_bi-100" PORT = int(os.getenv("PORT", "8080")) STRATEGY_ID = os.getenv("STRATEGY_ID", "") TARGET_GPU = os.getenv("MODELHUB_TARGET_GPU", DEFAULT_TARGET_GPU) AUTO_SCAN_ENABLED = os.getenv("AUTO_SCAN_ENABLED", "true").lower() not in { "0", "false", "no", "off", } AUTO_SUBMIT_ENABLED = os.getenv("AUTO_SUBMIT_ENABLED", "true").lower() not in { "0", "false", "no", "off", } SCAN_INTERVAL_SECONDS = max(int(os.getenv("SCAN_INTERVAL_SECONDS", "3600")), 60) MAX_BODY_BYTES = 1_000_000 CANDIDATE_ENDPOINT = "https://modelhub.org.cn/api/computility/models/top/models" TASK_PAGE_ENDPOINT = "https://modelhub.org.cn/api/adapt/task/page" TASK_ADD_ENDPOINT = "https://modelhub.org.cn/api/adapt/task/add" DEFAULT_CONFIG_PARAMS = """framework: vllm api: completion lang: zh max_model_len: 2048 max_tokens: 256 temperature: 0.1 repetition_penalty: 1.0 top_p: 0.9 sut_config: gpu_num: 1 values: command: - python3 - -m - vllm.entrypoints.openai.api_server - --host - 0.0.0.0 - --port - '20644' - --served-model-name - llm - --model - /model - --max-model-len - '2048' - --tensor-parallel-size - '1' - --max-num-seqs - '8' - --enforce-eager - --disable-log-requests - --enable-prefix-caching - --trust-remote-code ref_config: gpu_num: 1 values: command: - vllm - serve - /model - --port - '80' - --served-model-name - llm - --max-model-len - '2048' - -tp - '1' - --enforce-eager - --trust-remote-code """ CONFIG_PARAMS = os.getenv("MODELHUB_CONFIG_PARAMS", DEFAULT_CONFIG_PARAMS) SECRET_ASSIGNMENT_RE = re.compile( r"(?i)(token|secret|password|api[_-]?key)=([^&\s]+)" ) # Keep urllib's standard proxy handling. The hosted runtime may provide its # outbound route through HTTP(S)_PROXY, so disabling proxies can isolate the # scanner even though the container itself remains healthy. HTTP_OPENER = build_opener() STATE_LOCK = threading.Lock() STOP = threading.Event() SCANNER_STATE: dict[str, Any] = { "enabled": AUTO_SCAN_ENABLED, "running": False, "ready": False, "last_scan_at": None, "last_candidate": None, "last_action": "not_started", "last_error_type": None, } class ValidationError(ValueError): """Raised when an incoming analysis request is invalid.""" class PlatformAPIError(RuntimeError): """Raised when ModelHub returns a non-success business response.""" TOKEN_PLACEHOLDERS = {"tmp", "placeholder", "changeme", "change-me", "test"} def _valid_runtime_token(value: str) -> bool: value = value.strip() return bool(value) and value.lower() not in TOKEN_PLACEHOLDERS def resolve_runtime_token( environ: Mapping[str, str] | None = None, read_text: Callable[[str], str] | None = None, ) -> str: """Resolve the platform credential without ever logging its value.""" source = environ if environ is not None else os.environ token_file = source.get("XC_TOKEN_FILE", "").strip() if token_file: file_reader = read_text or ( lambda path: Path(path).read_text(encoding="utf-8") ) try: value = file_reader(token_file).strip() except (OSError, UnicodeError): return "" return value if _valid_runtime_token(value) else "" for name in ("MODELHUB_XC_TOKEN", "XC_TOKEN", "EXTERNAL_SERVICE_TOKEN"): value = source.get(name, "").strip() if _valid_runtime_token(value): return value if value: return "" return "" XC_TOKEN = resolve_runtime_token() def _safe_text(value: Any, limit: int = 200) -> str: text = str(value or "").strip() text = SECRET_ASSIGNMENT_RE.sub(r"\1=[REDACTED]", text) return text[:limit] def _safe_model_name(value: Any) -> str: text = _safe_text(value) or "未指定模型" if "://" in text: try: parsed = urlsplit(text) text = f"{parsed.scheme}://{parsed.hostname or ''}{parsed.path}" except ValueError: text = "无效模型地址" return text[:200] def _log_event(event: str, **fields: Any) -> None: record = { "timestamp": datetime.now(timezone.utc).isoformat(), "event": event, "agent": AGENT_NAME, "version": AGENT_VERSION, **fields, } print(json.dumps(record, ensure_ascii=False, separators=(",", ":")), flush=True) def _state_update(**fields: Any) -> None: with STATE_LOCK: SCANNER_STATE.update(fields) def scanner_snapshot() -> dict[str, Any]: with STATE_LOCK: return dict(SCANNER_STATE) def _http_json( method: str, url: str, *, payload: dict[str, Any] | None = None, token: str = "", ) -> dict[str, Any]: """Call one JSON endpoint without logging headers, bodies, or credentials.""" body = None headers = { "Accept": "application/json", "User-Agent": f"{AGENT_NAME}/{AGENT_VERSION}", } if payload is not None: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") headers["Content-Type"] = "application/json" if token: headers["Xc-Token"] = token request = Request(url, data=body, headers=headers, method=method) with HTTP_OPENER.open(request, timeout=20) as response: result = json.loads(response.read().decode("utf-8")) if not isinstance(result, dict): raise PlatformAPIError("unexpected_response_shape") return result def _require_success(payload: dict[str, Any], operation: str) -> None: code = payload.get("code") if code not in (None, 0, "0", 200, "200"): message = _safe_text(payload.get("message") or "platform_rejected", 120) raise PlatformAPIError(f"{operation}_failed:{code}:{message}") def _candidate_records(payload: dict[str, Any]) -> list[dict[str, str]]: data = payload.get("data") if not isinstance(data, list): return [] candidates: list[dict[str, str]] = [] for item in data: if not isinstance(item, dict): continue model_id = str(item.get("modelId") or "").strip() info = item.get("taskLevelsInfo") info = info if isinstance(info, dict) else {} raw_type = str( info.get("taskLevelName") or info.get("taskLevelCode") or info.get("taskLevelEnglishName") or "" ).lower() chinese_type = str(info.get("taskLevelChineseName") or "").strip() if model_id and ( "text-generation" in raw_type or "text_generation" in raw_type or chinese_type == "文本生成" ): candidates.append({"model_id": model_id, "task_type": "text-generation"}) return candidates def _model_size_score(model_id: str) -> float: match = re.search(r"(?i)(\d+(?:\.\d+)?)\s*([bm])(?:\b|[_-])", model_id) if not match: return 1_000_000.0 size = float(match.group(1)) return size * (1_000 if match.group(2).lower() == "b" else 1) def select_candidate(payload: dict[str, Any]) -> dict[str, str] | None: """Prefer a small text-generation model in a verified EngineX family.""" candidates = _candidate_records(payload) if not candidates: return None def score(item: dict[str, str]) -> tuple[int, float, str]: model_id = item["model_id"].lower() supported = any( family in model_id for family in ("qwen3", "llama3", "deepseek-r1-distill") ) return (0 if supported else 1, _model_size_score(model_id), model_id) return min(candidates, key=score) def _model_address(model_id: str) -> str: safe_path = "/".join(quote(part, safe="") for part in model_id.split("/")) return f"https://www.modelscope.cn/models/{safe_path}" def _history_records(payload: dict[str, Any]) -> list[Any]: data = payload.get("data") if not isinstance(data, dict): return [] records = data.get("records") return records if isinstance(records, list) else [] def scan_once(request_json: Any = None) -> dict[str, Any]: """Discover, deduplicate, and submit at most one adaptation task.""" call = request_json or _http_json scan_at = datetime.now(timezone.utc).isoformat() ready = bool( AUTO_SUBMIT_ENABLED and XC_TOKEN and STRATEGY_ID and TARGET_GPU and CONFIG_PARAMS ) _state_update( running=True, ready=ready, last_scan_at=scan_at, last_error_type=None, ) _log_event("candidate_scan_started", source="modelhub_hot_models") candidate_payload = call("GET", CANDIDATE_ENDPOINT) _require_success(candidate_payload, "candidate_query") candidate = select_candidate(candidate_payload) if candidate is None: result = {"action": "no_candidate"} _state_update(running=False, last_candidate=None, last_action=result["action"]) _log_event("candidate_scan_completed", candidate_count=0, action=result["action"]) return result model_id = _safe_model_name(candidate["model_id"]) _state_update(last_candidate=model_id) _log_event( "candidate_discovered", model=model_id, task_type=candidate["task_type"], target_gpu=TARGET_GPU, ) missing: list[str] = [] if not AUTO_SUBMIT_ENABLED: missing.append("auto_submit_disabled") if not XC_TOKEN: missing.append("runtime_token_missing") if not STRATEGY_ID: missing.append("strategy_id_missing") if not TARGET_GPU: missing.append("target_gpu_missing") if not CONFIG_PARAMS: missing.append("task_config_missing") if missing: result = {"action": "submission_blocked", "reasons": missing, "model": model_id} _state_update(running=False, ready=False, last_action=result["action"]) _log_event("task_submission_blocked", model=model_id, reasons=missing) return result query = urlencode( { "current": 1, "pageSize": 20, "onlyMine": "true", "modelId": candidate["model_id"], "gpuType": TARGET_GPU, } ) history = call("GET", f"{TASK_PAGE_ENDPOINT}?{query}", token=XC_TOKEN) _require_success(history, "history_query") if _history_records(history): result = {"action": "duplicate_skipped", "model": model_id} _state_update(running=False, last_action=result["action"]) _log_event("task_duplicate_skipped", model=model_id, target_gpu=TARGET_GPU) return result task_payload = { "modelAddress": _model_address(candidate["model_id"]), "taskType": candidate["task_type"], "targetGpu": TARGET_GPU, "framework": "vllm", "strategyId": STRATEGY_ID, "configParams": CONFIG_PARAMS, } response = call("POST", TASK_ADD_ENDPOINT, payload=task_payload, token=XC_TOKEN) _require_success(response, "task_create") response_data = response.get("data") task_id = None if isinstance(response_data, dict): task_id = response_data.get("id") or response_data.get("taskId") elif response_data not in (None, ""): task_id = response_data result = {"action": "submitted", "model": model_id, "task_id": task_id} _state_update(running=False, last_action=result["action"]) _log_event("task_submitted", model=model_id, target_gpu=TARGET_GPU, task_id=task_id) return result def build_adaptation_plan(payload: dict[str, Any]) -> dict[str, Any]: """Return the concrete adaptation plan used by this worker.""" if not isinstance(payload, dict): raise ValidationError("请求正文必须是 JSON 对象") model = _safe_model_name( payload.get("model_name") or payload.get("model") or payload.get("model_address") ) return { "agent": AGENT_NAME, "version": AGENT_VERSION, "model": model, "target": { "vendor": TARGET_VENDOR, "card": TARGET_CARD, "platform_gpu": TARGET_GPU, "cards": 1, }, "framework": "vllm", "task_type": "text-generation", "execution": [ "查询同模型与卡型的现有验证任务", "生成单卡 EngineX/vLLM 验证配置", "提交平台验证任务", "由平台执行模型加载、推理与指标采集", ], "automatic_submission_ready": bool( AUTO_SUBMIT_ENABLED and XC_TOKEN and STRATEGY_ID and TARGET_GPU and CONFIG_PARAMS ), } def _scanner_loop() -> None: _log_event( "scanner_started", interval_seconds=SCAN_INTERVAL_SECONDS, auto_submit=AUTO_SUBMIT_ENABLED, runtime_token_present=bool(XC_TOKEN), strategy_id_present=bool(STRATEGY_ID), target_gpu=TARGET_GPU, ) while not STOP.is_set(): try: scan_once() except Exception as exc: # boundary: keep health endpoint alive error_type = type(exc).__name__ _state_update( running=False, ready=False, last_action="scan_error", last_error_type=error_type, ) _log_event("scanner_error", error_type=error_type) if STOP.wait(SCAN_INTERVAL_SECONDS): break class Handler(BaseHTTPRequestHandler): server_version = "ModelHubAutoAdaptationAgent/1.0" def _json(self, payload: dict[str, Any], status: int = 200) -> None: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self) -> None: # noqa: N802 path = urlsplit(self.path).path if path == "/health": self._json( { "status": "ok", "agent": AGENT_NAME, "version": AGENT_VERSION, "scanner": scanner_snapshot(), } ) return if path == "/scanner": self._json(scanner_snapshot()) return if path == "/": self._json( { "name": AGENT_NAME, "version": AGENT_VERSION, "description": "自动发现模型并提交国产卡适配验证任务", "target": {"vendor": TARGET_VENDOR, "card": TARGET_CARD}, "endpoints": ["GET /health", "GET /scanner", "POST /analyze"], "automatic_submission_ready": bool( AUTO_SUBMIT_ENABLED and XC_TOKEN and STRATEGY_ID and TARGET_GPU and CONFIG_PARAMS ), } ) return self._json({"error": "not_found"}, 404) def do_POST(self) -> None: # noqa: N802 if urlsplit(self.path).path != "/analyze": self._json({"error": "not_found"}, 404) return try: length = int(self.headers.get("Content-Length", "0")) if length <= 0: raise ValidationError("请求正文不能为空") if length > MAX_BODY_BYTES: self._json({"error": "payload_too_large"}, 413) return payload = json.loads(self.rfile.read(length)) model = _safe_model_name(payload.get("model_name") if isinstance(payload, dict) else None) _log_event("adaptation_plan_started", model=model) plan = build_adaptation_plan(payload) _log_event("adaptation_plan_completed", model=plan["model"]) self._json(plan) except (json.JSONDecodeError, UnicodeDecodeError): self._json({"error": "invalid_json"}, 400) except ValidationError as exc: self._json({"error": "validation_error", "message": str(exc)}, 422) def log_message(self, _format: str, *args: Any) -> None: status = str(args[1]) if len(args) > 1 else "unknown" _log_event( "http_access", method=self.command, path=urlsplit(self.path).path, status=status, ) def _handle_signal(signum: int, _frame: Any) -> None: _log_event("shutdown_requested", signal=signum) STOP.set() def main() -> None: signal.signal(signal.SIGTERM, _handle_signal) signal.signal(signal.SIGINT, _handle_signal) server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler) server.timeout = 1 _log_event( "service_started", host="0.0.0.0", port=PORT, target_vendor=TARGET_VENDOR, target_card=TARGET_CARD, ) scanner_thread = None if AUTO_SCAN_ENABLED: scanner_thread = threading.Thread( target=_scanner_loop, name="modelhub-auto-adaptation-scanner", daemon=True, ) scanner_thread.start() else: _state_update(last_action="disabled") _log_event("scanner_disabled", reason="auto_scan_disabled") while not STOP.is_set(): server.handle_request() server.server_close() if scanner_thread is not None: scanner_thread.join(timeout=5) if __name__ == "__main__": main()