1919 "RATE_LIMIT_STATUSES" ,
2020 "extract_error_code" ,
2121 "parse_module_error_string" ,
22+ "get_user_friendly_message" ,
2223)
2324
2425
@@ -73,12 +74,61 @@ class GenVMErrorCode(StrEnum):
7374 WEB_REQUEST_FAILED = "WEB_REQUEST_FAILED"
7475 WEB_TLD_FORBIDDEN = "WEB_TLD_FORBIDDEN"
7576
77+ # Contract/runner errors
78+ INVALID_RUNNER = "INVALID_RUNNER"
79+
7680 # Execution errors
7781 GENVM_TIMEOUT = "GENVM_TIMEOUT"
7882 CONTRACT_ERROR = "CONTRACT_ERROR"
7983 INTERNAL_ERROR = "INTERNAL_ERROR"
8084
8185
86+ # User-friendly error messages for each error code
87+ ERROR_CODE_MESSAGES : dict [str , str ] = {
88+ GenVMErrorCode .INVALID_RUNNER : (
89+ "Invalid runner version. The 'latest' and 'test' versions are not allowed. "
90+ "Please use a fixed version hash in your contract header, e.g.: "
91+ '# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }'
92+ ),
93+ GenVMErrorCode .LLM_RATE_LIMITED : "LLM provider rate limit exceeded. Please try again later." ,
94+ GenVMErrorCode .LLM_NO_PROVIDER : "No LLM provider available for the requested operation." ,
95+ GenVMErrorCode .LLM_PROVIDER_ERROR : "LLM provider returned an error." ,
96+ GenVMErrorCode .LLM_INVALID_API_KEY : "Invalid API key for the LLM provider." ,
97+ GenVMErrorCode .LLM_TIMEOUT : "LLM request timed out." ,
98+ GenVMErrorCode .WEB_REQUEST_FAILED : "Web request failed." ,
99+ GenVMErrorCode .WEB_TLD_FORBIDDEN : "Access to the requested domain is forbidden." ,
100+ GenVMErrorCode .GENVM_TIMEOUT : "Contract execution timed out." ,
101+ GenVMErrorCode .CONTRACT_ERROR : "Contract execution error." ,
102+ GenVMErrorCode .INTERNAL_ERROR : "Internal GenVM error." ,
103+ }
104+
105+
106+ def get_user_friendly_message (
107+ error_code : str | None , original_message : str , raw_error : dict | None = None
108+ ) -> str :
109+ """
110+ Get a user-friendly error message based on the error code.
111+
112+ Args:
113+ error_code: The standardized error code (e.g., INVALID_RUNNER)
114+ original_message: The original error message from GenVM
115+ raw_error: Optional raw error structure with causes
116+
117+ Returns:
118+ A user-friendly error message
119+ """
120+ if error_code and error_code in ERROR_CODE_MESSAGES :
121+ return ERROR_CODE_MESSAGES [error_code ]
122+
123+ # Fallback: check raw_error causes for invalid runner pattern
124+ if raw_error and isinstance (raw_error .get ("causes" ), list ):
125+ for cause in raw_error ["causes" ]:
126+ if isinstance (cause , str ) and "invalid runner id" in cause .lower ():
127+ return ERROR_CODE_MESSAGES [GenVMErrorCode .INVALID_RUNNER ]
128+
129+ return original_message
130+
131+
82132# Mapping from Lua error causes to standardized error codes
83133LUA_CAUSE_TO_CODE : dict [str , GenVMErrorCode ] = {
84134 "NO_PROVIDER_FOR_PROMPT" : GenVMErrorCode .LLM_NO_PROVIDER ,
@@ -214,6 +264,9 @@ def extract_error_code(
214264 # Map Lua causes to error codes
215265 if isinstance (causes , list ):
216266 for cause in causes :
267+ # Check for invalid runner error (e.g., "invalid runner id: py-genlayer:latest")
268+ if isinstance (cause , str ) and "invalid runner id" in cause .lower ():
269+ return GenVMErrorCode .INVALID_RUNNER
217270 if cause in LUA_CAUSE_TO_CODE :
218271 # Special case: STATUS_NOT_OK might be rate limiting based on ctx
219272 if cause == "STATUS_NOT_OK" :
@@ -230,6 +283,10 @@ def _extract_from_message(message: str, stderr: str = "") -> Optional[str]:
230283 """Extract error code from message string or stderr."""
231284 combined = f"{ message } { stderr } " .lower ()
232285
286+ # Check for invalid runner (e.g., using :latest or :test in non-debug mode)
287+ if "invalid runner" in combined :
288+ return GenVMErrorCode .INVALID_RUNNER
289+
233290 # Check for specific error patterns
234291 if "rate limit" in combined or "429" in combined :
235292 return GenVMErrorCode .LLM_RATE_LIMITED
0 commit comments