feat: add multi-provider authentication and inference provider selection
- Implemented a multi-provider authentication system for the Hermes Agent, supporting OAuth for Nous Portal and traditional API key methods for OpenRouter and custom endpoints. - Enhanced CLI with commands for logging in and out of providers, allowing users to authenticate and manage their credentials easily. - Updated configuration options to select inference providers, with detailed documentation on usage and setup. - Improved status reporting to include authentication status and provider details, enhancing user awareness of their current configuration. - Added new files for authentication handling and updated existing components to integrate the new provider system.
This commit is contained in:
parent
c007b9e5bd
commit
cfef34f7a6
9 changed files with 1639 additions and 113 deletions
103
run_agent.py
103
run_agent.py
|
|
@ -2031,7 +2031,96 @@ class AIAgent:
|
|||
# Silent fail - don't interrupt the agent for debug logging
|
||||
if self.verbose_logging:
|
||||
logging.warning(f"Failed to log API payload: {e}")
|
||||
|
||||
|
||||
def _mask_api_key_for_logs(self, key: Optional[str]) -> Optional[str]:
|
||||
if not key:
|
||||
return None
|
||||
if len(key) <= 12:
|
||||
return "***"
|
||||
return f"{key[:8]}...{key[-4:]}"
|
||||
|
||||
def _dump_api_request_debug(
|
||||
self,
|
||||
api_kwargs: Dict[str, Any],
|
||||
*,
|
||||
reason: str,
|
||||
error: Optional[Exception] = None,
|
||||
) -> Optional[Path]:
|
||||
"""
|
||||
Dump a debug-friendly HTTP request record for chat.completions.create().
|
||||
|
||||
Captures the request body from api_kwargs (excluding transport-only keys
|
||||
like timeout). Intended for debugging provider-side 4xx failures where
|
||||
retries are not useful.
|
||||
"""
|
||||
try:
|
||||
body = copy.deepcopy(api_kwargs)
|
||||
body.pop("timeout", None)
|
||||
body = {k: v for k, v in body.items() if v is not None}
|
||||
|
||||
api_key = None
|
||||
try:
|
||||
api_key = getattr(self.client, "api_key", None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
dump_payload: Dict[str, Any] = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"session_id": self.session_id,
|
||||
"reason": reason,
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": f"{self.base_url.rstrip('/')}/chat/completions",
|
||||
"headers": {
|
||||
"Authorization": f"Bearer {self._mask_api_key_for_logs(api_key)}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"body": body,
|
||||
},
|
||||
}
|
||||
|
||||
if error is not None:
|
||||
error_info: Dict[str, Any] = {
|
||||
"type": type(error).__name__,
|
||||
"message": str(error),
|
||||
}
|
||||
for attr_name in ("status_code", "request_id", "code", "param", "type"):
|
||||
attr_value = getattr(error, attr_name, None)
|
||||
if attr_value is not None:
|
||||
error_info[attr_name] = attr_value
|
||||
|
||||
body_attr = getattr(error, "body", None)
|
||||
if body_attr is not None:
|
||||
error_info["body"] = body_attr
|
||||
|
||||
response_obj = getattr(error, "response", None)
|
||||
if response_obj is not None:
|
||||
try:
|
||||
error_info["response_status"] = getattr(response_obj, "status_code", None)
|
||||
error_info["response_text"] = response_obj.text
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
dump_payload["error"] = error_info
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
dump_file = self.logs_dir / f"request_dump_{self.session_id}_{timestamp}.json"
|
||||
dump_file.write_text(
|
||||
json.dumps(dump_payload, ensure_ascii=False, indent=2, default=str),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print(f"{self.log_prefix}🧾 Request debug dump written to: {dump_file}")
|
||||
|
||||
if os.getenv("HERMES_DUMP_REQUEST_STDOUT", "").strip().lower() in {"1", "true", "yes", "on"}:
|
||||
print(json.dumps(dump_payload, ensure_ascii=False, indent=2, default=str))
|
||||
|
||||
return dump_file
|
||||
except Exception as dump_error:
|
||||
if self.verbose_logging:
|
||||
logging.warning(f"Failed to dump API request debug payload: {dump_error}")
|
||||
return None
|
||||
|
||||
def _save_session_log(self, messages: List[Dict[str, Any]] = None):
|
||||
"""
|
||||
Save the current session trajectory to the logs directory.
|
||||
|
|
@ -2425,7 +2514,10 @@ class AIAgent:
|
|||
|
||||
if extra_body:
|
||||
api_kwargs["extra_body"] = extra_body
|
||||
|
||||
|
||||
if os.getenv("HERMES_DUMP_REQUESTS", "").strip().lower() in {"1", "true", "yes", "on"}:
|
||||
self._dump_api_request_debug(api_kwargs, reason="preflight")
|
||||
|
||||
response = self.client.chat.completions.create(**api_kwargs)
|
||||
|
||||
api_duration = time.time() - api_start_time
|
||||
|
|
@ -2624,7 +2716,9 @@ class AIAgent:
|
|||
# Check for non-retryable client errors (4xx HTTP status codes).
|
||||
# These indicate a problem with the request itself (bad model ID,
|
||||
# invalid API key, forbidden, etc.) and will never succeed on retry.
|
||||
is_client_error = any(phrase in error_msg for phrase in [
|
||||
status_code = getattr(api_error, "status_code", None)
|
||||
is_client_status_error = isinstance(status_code, int) and 400 <= status_code < 500
|
||||
is_client_error = is_client_status_error or any(phrase in error_msg for phrase in [
|
||||
'error code: 400', 'error code: 401', 'error code: 403',
|
||||
'error code: 404', 'error code: 422',
|
||||
'is not a valid model', 'invalid model', 'model not found',
|
||||
|
|
@ -2633,6 +2727,9 @@ class AIAgent:
|
|||
])
|
||||
|
||||
if is_client_error:
|
||||
self._dump_api_request_debug(
|
||||
api_kwargs, reason="non_retryable_client_error", error=api_error,
|
||||
)
|
||||
print(f"{self.log_prefix}❌ Non-retryable client error detected. Aborting immediately.")
|
||||
print(f"{self.log_prefix} 💡 This type of error won't be fixed by retrying.")
|
||||
logging.error(f"{self.log_prefix}Non-retryable client error: {api_error}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue