
    }-j                       U d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
ZddlZddlmZmZ ddlmZ ddlmZ ddlmZ ddlmZmZmZmZmZmZmZmZmZmZm Z   ej!        e"          Z#dqdZ$dZ%dZ&dZ' e(ddh          Z)dZ*dZ+da,de-d<   drdZ.dsdZ/dZ0i dddd d!d"d#d$d%d&d'd(d)d*d+d,d-d.d/d0d1d2d3d4d5d6d7d8d9d:d;d<d=d>d?d@iZ1dAe-dB<   d=dCiZ2dDe-dE<    e(h dF          Z3dtdHZ4dudJZ5dvdMZ6dwdNZ7dxdPZ8dydQZ9dzdUZ:d{dXZ;d|dYZ<d}d\Z=ddd]d~daZ>ddcZ?dddZ@ddeddfZAddgZBddjZCdddkZDdwdlZEddmZFddnZGddoZHddpZIdS )zHermes Web UI -- provider management endpoints.

Provides CRUD operations for configuring provider API keys post-onboarding.
Closes #586 (allow provider key update) and part of #604 (model picker
multi-provider support).
    )annotationsN)datetimetimezone)Path)SimpleNamespace)Any)_PROVIDER_DISPLAY_PROVIDER_MODELS_custom_provider_slug_from_name_get_label_for_model_models_from_live_provider_ids_read_live_provider_model_ids#_read_visible_codex_cache_model_ids_save_yaml_config_file
get_configinvalidate_models_cachereload_configprovider_idstrnameobjectreturnboolc                D   t          | pd                                                                          }t          |pd                                                                          }|r|sdS t          |          }|d| h}|r|                    |           ||v S )zAReturn True when *provider_id* refers to a named custom provider. Fzcustom:)r   striplowerr   add)r   r   pidraw_nameslug
candidatess         #/root/hermes-webui/api/providers.py_custom_provider_name_matchesr$   )   s    
kR
 
 
&
&
(
(
.
.
0
0C4:2$$&&,,..H h u*844D0h001J t*    z https://openrouter.ai/api/v1/keyg      @g     A@openai-codex	anthropic   zimport sys
try:
    import ctypes, signal
    libc = ctypes.CDLL(None)
    libc.prctl(1, signal.SIGTERM)   # PR_SET_DEATHSIG=1, SIGTERM=15
except Exception:
    pass
z!threading.BoundedSemaphore | None_account_usage_probe_semaphorethreading.BoundedSemaphorec                 P    t           t          j        t                    a t           S N)r)   	threadingBoundedSemaphore$_MAX_CONCURRENT_ACCOUNT_USAGE_PROBES r%   r#   "_get_account_usage_probe_semaphorer1   f   s&    %-)2)C0*
 *
& *)r%   Nonec                     	 dd l } |                     d           }|                    dt          j                   d S # t
          $ r Y d S w xY w)Nr      )ctypesCDLLprctlsignalSIGTERM	Exception)r5   libcs     r#   _account_usage_preexec_fnr<   v   s_    {{4  

1fn%%%%%   s   9= 
A
Aa?  
import base64
import json
import sys
from datetime import datetime, timezone
from types import SimpleNamespace
from urllib import request as urllib_request

from agent.account_usage import fetch_account_usage


_CODEX_DEFAULT_BASE_URL = "https://chatgpt.com/backend-api/codex"


def _iso(value):
    if value in (None, ""):
        return None
    if hasattr(value, "isoformat"):
        text = value.isoformat()
        return text.replace("+00:00", "Z")
    text = str(value).strip()
    return text or None


def _snapshot_payload(snapshot):
    if snapshot is None:
        return None
    windows = []
    for window in getattr(snapshot, "windows", ()) or ():
        windows.append({
            "label": str(getattr(window, "label", "") or ""),
            "used_percent": getattr(window, "used_percent", None),
            "reset_at": _iso(getattr(window, "reset_at", None)),
            "detail": getattr(window, "detail", None),
        })
    return {
        "provider": str(getattr(snapshot, "provider", "") or ""),
        "source": str(getattr(snapshot, "source", "") or ""),
        "title": str(getattr(snapshot, "title", "") or ""),
        "plan": getattr(snapshot, "plan", None),
        "windows": windows,
        "details": list(getattr(snapshot, "details", ()) or ()),
        "available": bool(getattr(snapshot, "available", bool(windows))),
        "unavailable_reason": getattr(snapshot, "unavailable_reason", None),
        "fetched_at": _iso(getattr(snapshot, "fetched_at", None)),
    }


def _snapshot_available(snapshot):
    if snapshot is None:
        return False
    try:
        return bool(getattr(snapshot, "available", False))
    except Exception:
        return False


def _number(value):
    if isinstance(value, bool) or value is None:
        return None
    if isinstance(value, (int, float)):
        return value
    try:
        text = str(value).strip()
        if not text:
            return None
        number = float(text)
        return int(number) if number.is_integer() else number
    except Exception:
        return None


def _parse_dt(value):
    if value in (None, ""):
        return None
    if isinstance(value, (int, float)):
        try:
            return datetime.fromtimestamp(float(value), tz=timezone.utc)
        except Exception:
            return None
    text = str(value).strip()
    if not text:
        return None
    if text.endswith("Z"):
        text = text[:-1] + "+00:00"
    try:
        dt = datetime.fromisoformat(text)
    except ValueError:
        return None
    return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)


def _title_case_slug(value):
    cleaned = str(value or "").strip()
    if not cleaned:
        return None
    return cleaned.replace("_", " ").replace("-", " ").title()


def _resolve_codex_usage_url(base_url):
    normalized = str(base_url or "").strip().rstrip("/") or _CODEX_DEFAULT_BASE_URL
    if normalized.endswith("/codex"):
        normalized = normalized[: -len("/codex")]
    if "/backend-api" in normalized:
        return normalized + "/wham/usage"
    return normalized + "/api/codex/usage"


def _jwt_claims(token):
    if not isinstance(token, str) or token.count(".") != 2:
        return {}
    payload = token.split(".")[1]
    payload += "=" * ((4 - len(payload) % 4) % 4)
    try:
        claims = json.loads(base64.urlsafe_b64decode(payload.encode("utf-8")).decode("utf-8"))
    except Exception:
        return {}
    return claims if isinstance(claims, dict) else {}


def _codex_usage_headers(access_token):
    headers = {
        "Authorization": "Bearer " + access_token,
        "Accept": "application/json",
        "User-Agent": "codex_cli_rs/0.0.0 (Hermes WebUI)",
        "originator": "codex_cli_rs",
    }
    auth_claim = _jwt_claims(access_token).get("https://api.openai.com/auth")
    account_id = None
    if isinstance(auth_claim, dict):
        account_id = auth_claim.get("chatgpt_account_id")
    if isinstance(account_id, str) and account_id.strip():
        headers["ChatGPT-Account-ID"] = account_id.strip()
    return headers


def _entry_value(entry, *names):
    for name in names:
        try:
            value = getattr(entry, name)
        except Exception:
            value = None
        if value in (None, ""):
            continue
        text = str(value).strip()
        if text:
            return text
    return None


def _codex_snapshot_from_usage_payload(payload):
    if not isinstance(payload, dict):
        payload = {}
    rate_limit = payload.get("rate_limit")
    if not isinstance(rate_limit, dict):
        rate_limit = {}
    windows = []
    for key, label in (("primary_window", "Session"), ("secondary_window", "Weekly")):
        window = rate_limit.get(key)
        if not isinstance(window, dict):
            continue
        used = _number(window.get("used_percent"))
        if used is None:
            continue
        windows.append(SimpleNamespace(
            label=label,
            used_percent=float(used),
            reset_at=_parse_dt(window.get("reset_at")),
            detail=None,
        ))

    details = []
    credits = payload.get("credits")
    if isinstance(credits, dict) and credits.get("has_credits"):
        balance = _number(credits.get("balance"))
        if balance is not None:
            details.append("Credits balance: $" + format(float(balance), ".2f"))
        elif credits.get("unlimited"):
            details.append("Credits balance: unlimited")

    return SimpleNamespace(
        provider="openai-codex",
        source="usage_api",
        title="Account limits",
        plan=_title_case_slug(payload.get("plan_type")),
        windows=tuple(windows),
        details=tuple(details),
        available=bool(windows or details),
        unavailable_reason=None,
        fetched_at=datetime.now(timezone.utc),
    )


def _fetch_codex_account_usage_from_pool():
    try:
        from agent.credential_pool import load_pool

        pool = load_pool("openai-codex")
        entry = pool.select() if pool is not None else None
        if entry is None:
            return None
        access_token = _entry_value(entry, "runtime_api_key", "access_token")
        if not access_token:
            return None
        base_url = _entry_value(entry, "runtime_base_url", "base_url") or _CODEX_DEFAULT_BASE_URL
        request = urllib_request.Request(
            _resolve_codex_usage_url(base_url),
            headers=_codex_usage_headers(access_token),
        )
        with urllib_request.urlopen(request, timeout=15.0) as response:
            payload = json.loads(response.read().decode("utf-8") or "{}")
        return _codex_snapshot_from_usage_payload(payload)
    except Exception:
        return None


provider = sys.argv[1]
api_key = sys.argv[2] or None
try:
    snapshot = fetch_account_usage(provider, api_key=api_key)
except Exception:
    snapshot = None
if str(provider or "").strip().lower() == "openai-codex" and not _snapshot_available(snapshot):
    fallback_snapshot = _fetch_codex_account_usage_from_pool()
    if _snapshot_available(fallback_snapshot) or snapshot is None:
        snapshot = fallback_snapshot
print(json.dumps(_snapshot_payload(snapshot)))

openrouterOPENROUTER_API_KEYANTHROPIC_API_KEYopenaiOPENAI_API_KEYgoogleGOOGLE_API_KEYgeminiGEMINI_API_KEYzaiGLM_API_KEYzkimi-codingKIMI_API_KEYdeepseekDEEPSEEK_API_KEYminimaxMINIMAX_API_KEYz
minimax-cnMINIMAX_CN_API_KEY	mistralaiMISTRAL_API_KEYzx-aiXAI_API_KEYxiaomiXIAOMI_API_KEYzopencode-zenOPENCODE_ZEN_API_KEYzopencode-goOPENCODE_GO_API_KEYzollama-cloudOLLAMA_API_KEYlmstudio
LM_API_KEYnvidiaNVIDIA_API_KEYdict[str, str]_PROVIDER_ENV_VAR)LMSTUDIO_API_KEYzdict[str, tuple[str, ...]]_PROVIDER_ENV_VAR_ALIASES>   
qwen-oauthcopilot-acpnouscopilotr&   r   c                 p    	 ddl m}   |             S # t          $ r t          j                    dz  cY S w xY w)z(Return the active Hermes home directory.r   get_active_hermes_homez.hermes)api.profilesrd   ImportErrorr   homerc   s    r#   _get_hermes_homerh     sZ    '777777%%''' ' ' 'y{{Y&&&&'s     55env_pathc                   i }|                                  s|S 	 |                     d                                          D ]}|                                }|r|                    d          sd|vr2|                    dd          \  }}|                                                    d                              d          ||                                <   n# t          $ r i cY S w xY w|S )z&Read key=value pairs from a .env file.utf-8encoding#=r4   "')exists	read_text
splitlinesr   
startswithsplitr:   )ri   valuesrawlinekeyvalues         r#   _load_env_filer|     s   F?? %%w%77BBDD 	F 	FC99;;D 4??3// 3d??C++JC"'++--"5"5c":":"@"@"E"EF399;;	F    			Ms   CC C-,C-updatesdict[str, str | None]c                >   ddl m} ddl}|5  g }|                                 r<	 |                     d                                          }n# t          $ r g }Y nw xY wi }t          |          D ]g\  }}|                                }|rL|	                    d          s7d|v r3|
                    dd          d                                         }	|||	<   ht          |          }
g }|                                D ]\  }}|0t          j                            |d           ||v rd|
||         <   7t!          |                                          }|s[d	|v sd
|v rt#          d          |t          j        |<   ||v r| d| |
||         <   |                    | d|            d |
D             }
|rJ|
r3|
d                                         dk    r|
                    d           |
                    |           | j                            dd           d	                    |
          }|r|d	z  }|j        |j        z  }ddl}|                    t!          | j                  dd          \  }}	 t          j        |dd          5 }|                    |           |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j         ||           t          j!        ||            n5# tD          $ r( 	 t          j#        |           n# tH          $ r Y nw xY w w xY w	 |                      |           n# tH          $ r Y nw xY wddd           dS # 1 swxY w Y   dS )u*  Write key=value pairs to the .env file.

    Values of ``None`` cause the key to be removed.

    Preserves comments, blank lines, and original key order (#1164).
    New keys are appended at the end of the file with a blank-line separator.

    Holds ``_ENV_LOCK`` from ``api.streaming`` for the entire load → modify →
    write cycle to prevent TOCTOU races between concurrent POST /api/providers
    calls (each reading the same file baseline and overwriting the other's key).
    Also serialises os.environ mutations with streaming sessions.
    r   )	_ENV_LOCKNrk   rl   rn   ro   r4   
,API key must not contain newline characters.c                    g | ]}||S r,   r0   ).0ls     r#   
<listcomp>z#_write_env_file.<locals>.<listcomp>  s    AAAa1====r%   r   T)parentsexist_okz.env_z.tmp)dirprefixsuffixw)%api.streamingr   statrr   rs   rt   r:   	enumerater   ru   rv   listitemsosenvironpopr   
ValueErrorappendextendparentmkdirjoinS_IRUSRS_IWUSRtempfilemkstempfdopenwriteflushfsyncfilenochmodreplaceBaseExceptionunlinkOSError)ri   r}   r   _statexisting_linesexisting_key_indices_i_raw	_stripped_existing_key_indices_keyoutput_linesnew_keysrz   r{   cleancontent_mode	_tempfile_tmp_fd	_tmp_path_fs                        r#   _write_env_filer     s    ('''''	 O O$&?? 	$$!)!3!3W!3!E!E!P!P!R!R $ $ $!#$ 02!.11 	E 	EHB

I E!5!5c!:!: Esi?O?O,5OOC,C,CA,F,L,L,N,N)BD$%>?N++ !--// 	2 	2JC}
sD)))...>BL!5c!:;JJ$$&&E u}} !OPPP#BJsO***=@:J:J5:J:J1#6773 0 0 0 01111 BA<AAA  	* (R 0 6 6 8 8B > >##B''')))dT:::))L)) 	tOG -$$$$&..HO$$WV / 
 
	7C'::: &b!!!


%%%& & & & & & & & & & & & & & & HY&&&Jy(++++ 	 	 		)$$$$   		NN5!!!! 	 	 	D	]O O O O O O O O O O O O O O O O O Os   N(ANANAH"N L)AK3'L)3K7	7L):K7	;-L)(N)
M4M	M	
MMMMNM54N5
N?NNNNNc                v   t                               |           }|rt                      dz  }t          |          }|                    |          rdS t	          j        |          rdS t                              | d          pdD ]1}|                    |          r dS t	          j        |          r dS 2t                      }|                    di           }t          |t                    rt          |                    d          pd                                          rb|                    d          }|rKt          |                                                                          |                                 k    rdS |                    di           }t          |t                    rc|                    | i           }	t          |	t                    r8t          |	                    d          pd                                          rdS |                    d	g           }
t          |
t                    rv|
D ]s}t          |t                    r\t          | |                    d
                    r9t          |                    d          pd                                          r dS tdS )ux  Check whether a provider has a configured API key.

    Checks (in order):
    1. ``~/.hermes/.env`` for the known env var
    2. ``os.environ`` for the known env var
    3. ``config.yaml → model.api_key`` (only if provider is the active one)
    4. ``config.yaml → providers.<id>.api_key``
    5. ``config.yaml → custom_providers[].api_key`` (for custom providers)
    .envTr0   modelapi_keyr   provider	providerscustom_providersr   F)r[   getrh   r|   r   getenvr]   r   
isinstancedictr   r   r   r   r$   )r   env_varri   
env_valuesaliascfg	model_cfgactive_providerproviders_cfgprovider_cfgr   cps               r#   _provider_has_keyr   $  s     ##K00G #%%.#H--
>>'"" 	49W 	4 /22;CCIr 	 	E~~e$$ tty tt ,,C $$I)T"" s9==+C+C+Ir'J'J'P'P'R'R #--
33 	s?3399;;AACC{GXGXGZGZZZ4GGK,,M-&& $((b99lD)) 	c,2B2B92M2M2SQS.T.T.Z.Z.\.\ 	4ww1266"D)) $" 	$ 	$B"d## $0bffVnnMM $266),,23399;; $#tt5r%   
str | Nonec                   | pd                                                                 } t                              |           }|r:t	                      dz  }t          |          }|                    |          r)t          ||                                                    pdS t          j        |          r)t          j        |d                                           pdS t                              | d          pdD ]}|                    |          r+t          ||                                                    pdc S t          j        |          r+t          j        |d                                           pdc S t                      }|                    di           }t          |t                    rt          |                    d          pd                                                                           }t          |                    d          pd                                           }|r|| k    r|S |                    di           }	t          |	t                    re|	                    | i           }
t          |
t                    r:t          |
                    d          pd                                           }|r|S |                    d	g           }t          |t                    r|D ]}t          |t                    st          | |                    d
                    rt          |                    d          pd                                           }|                    d          rH|                    d          r3t          j        |dd         d                                           pdc S |r|c S dS )zDReturn a configured provider API key without exposing it to callers.r   r   Nr0   r   r   r   r   r   r   ${}r(   r   )r   r   r[   r   rh   r|   r   r   r   r]   r   r   r   r   r$   ru   endswith)r   r   ri   r   r   r   r   r   	model_keyr   r   provider_keyr   r   cp_keys                  r#   _get_provider_api_keyr   Y  s   $"++--3355K##K00G <#%%.#H--
>>'"" 	<z'*++1133;t;9W 	:9Wb))//119T9.22;CCIr 	< 	<E~~e$$ >:e,--3355====y <y++1133;t;;;< ,,C$$I)T"" immJ77=2>>DDFFLLNN	i006B77==??	 	K77GGK,,M-&& $$((b99lD)) 	$|//	::@bAAGGIIL $##ww1266"D)) 	"" 	" 	"Bb$'' ,["&&..II "RVVI..4"55;;==$$T** Gvs/C/C G9VAbD\266<<>>F$FFF "!MMM4r%   c                    t                      } |                     di           }t          |t                    sd S t	          |                    d          pd                                                                          }|pd S )Nr   r   r   )r   r   r   r   r   r   r   )r   r   r   s      r#   _active_provider_idr     sv    
,,C$$Ii&& t9==,,23399;;AACCHtr%   r{   r   int | float | Nonec                T   t          | t                    s| d S t          | t          t          f          r| S 	 t	          |                                           }|sd S t          |          }|                                rt          |          n|S # t          t          f$ r Y d S w xY wr,   )	r   r   intfloatr   r   
is_integer	TypeErrorr   )r{   textnumbers      r#   _quota_numberr     s    % %-t%#u&& 5zz!! 	4t$//11=s6{{{v=z"   tts   #B 3B B'&B'payloaddict[str, int | float | None]c                   t          | t                    r0t          |                     d          t                    r| d         } t          | t                    si } t          |                     d                    t          |                     d                    t          |                     d                    dS )Ndatalimit_remainingusagelimit)r   r   r   )r   r   r   r   )r   s    r#   _sanitize_openrouter_quotar     s    '4   "ZF0C0CT%J%J "&/gt$$ (5F)G)GHHw{{73344w{{73344  r%   c                ^   | dv rd S t          | t                    rn| j        r| n|                     t          j                  }|                    t          j                                                                      dd          S t          |           	                                }|pd S )N)Nr   )tzinfoz+00:00Z)
r   r   r   r   r   utc
astimezone	isoformatr   r   )r{   dtr   s      r#   _isoformat_utcr     s    
t%"" NlJUUX\(J(J}}X\**4466>>xMMMu::D<4r%   snapshotdict[str, Any] | Nonec                   | d S g }t          | dd          pdD ]}t          t          |dd          pd                                          }|s7t          t          |dd                     }d }|.t	          dt          ddt          |          z
                      }|                    |||t          t          |dd                     t          t          |d	d          pd                                          pd d
           d t          | dd          pdD             }t          t          | dd          pd                                          pd }t          t          | dd          pd                                          pd }t          t          | dd          pd                                          pd t          t          | dd          pd                                          pd t          t          | dd          pd                                          pd|||t          t          | dt          |p|                              o| |t          t          | dd                     d	S )Nwindowsr0   labelr   used_percentg        g      Y@reset_atdetail)r   r   remaining_percentr   r   c                    g | ]D}t          |                                          #t          |                                          ES r0   )r   r   )r   r   s     r#   r   z5_serialize_account_usage_snapshot.<locals>.<listcomp>  sR       v;;F  r%   detailsplanunavailable_reasonr   sourcetitleAccount limits	available
fetched_at	r   r  r  r   r   r   r  r  r  )
getattrr   r   r   maxminr   r   r   r   )	r   r   windowr   r   r   r   r   r  s	            r#   !_serialize_account_usage_snapshotr    s   t$&G(Ir228b  GFGR006B77==?? 	$WV^T%J%JKK # #CUEE,<O<O4O)P)P Q Q(!2&wvz4'H'HII'&(B77=2>>DDFFN$
 
 	 	 	 	 xB77=2  G
 wx,,23399;;CtDWX/CRHHNBOOUUWW_[_*b99?R@@FFHHPDgh"55;<<BBDDLWXw339r::@@BBVFV'(Kg>P9Q9QRRSSn\nXn0$WX|T%J%JKK
 
 
r%   base_urlr   r   r  r   c               *    ddl m}  || ||          S )Nr   )fetch_account_usager  )agent.account_usager  )r   r  r   r  s       r#   _agent_fetch_account_usager    s,    777777x(GLLLLr%   rg   c                   t          t          j                  }t          t	          |                     |d<   t          t	          |           dz                                            D ]\  }}|r|||<   t                              |pd	                                
                                          }|r|r|||<   	 ddlm} n# t          $ r d }Y nw xY wg }|r"|                    t          |                     |                    dd          }	|	r|                    |	           |r"t          j                            |          |d<   |S )NHERMES_HOMEr   r   r   )
_AGENT_DIR
PYTHONPATH)r   r   r   r   r   r|   r   r[   r   r   r   
api.configr  r:   r   pathsepr   )
rg   r   r   envrz   r{   r   r  pythonpath_partsexisting_pythonpaths
             r#   _account_usage_subprocess_envr    sq   
rz

CT$ZZC %T$ZZ&%899??AA  
U 	CH##X^$:$:$<$<$B$B$D$DEEG 7 G)))))))   


"$ 1J000'',33 5 3444 >JOO,<==LJs   C CCc                $   t          | t                    sd S t          d |                     d          pdD                       }t	          |                     d          |                     d          |                     d          |                     d          |t          |                     d          pd          t          |                     d	                    |                     d
          |                     d          	  	        S )Nc           	   3     K   | ]x}t          |t                    t          |                    d           |                    d          |                    d          |                    d                    V  ydS )r   r   r   r   )r   r   r   r   N)r   r   r   r   )r   r  s     r#   	<genexpr>z5_account_usage_payload_to_snapshot.<locals>.<genexpr>  s       	 	 fd##	**W%%N33ZZ
++::h''		
 	
 	
	 	 	 	 	 	r%   r   r0   r   r  r  r   r   r  r  r  r  )r   r   tupler   r   r   )r   r   s     r#   "_account_usage_payload_to_snapshotr!    s   gt$$ t 	 	 {{9--3	 	 	 	 	G Z(({{8$$kk'""[[  gkk),,233w{{;//00";;';<<;;|,,
 
 
 
r%   r   c          	     H   	 ddl m} n# t          $ r t          j        pd}Y nw xY w	 t
          j        t
          j        t
          j        dt          dd}t          t          d          r
t          |d<   t          j        |d	t          t          z   | |pd
gfdt          || |          i|}nZ# t
          j        $ r t"                              d|            Y d S t          $ r! t"                              d| d           Y d S w xY w|j        dk    r#t"                              d| |j                   d S 	 t)          j        |j        pd
                                pd          }n1# t(          j        $ r t"                              d|            Y d S w xY wt3          |          S )Nr   )
PYTHON_EXEpython3TF)stdinstdoutstderrr   timeoutcheckfork
preexec_fnz-cr   r  z$Account usage probe for %s timed outz+Account usage probe for %s failed to launchexc_infoz0Account usage probe for %s exited with status %snullz0Account usage probe for %s returned invalid JSON)r  r$  r:   sys
executable
subprocessDEVNULLPIPE)_ACCOUNT_USAGE_SUBPROCESS_TIMEOUT_SECONDShasattrr   r<   run(_ACCOUNT_USAGE_PARENT_DEATHSIG_BOOTSTRAP_ACCOUNT_USAGE_SUBPROCESS_CODEr  TimeoutExpiredloggerdebug
returncodejsonloadsr'  r   JSONDecodeErrorr!  )r   rg   r   r$  kwargsprocr   s          r#   #_agent_fetch_account_usage_for_homerC    s   1))))))) 1 1 1^0y


1
  ' o o@"
 "
 2v 	=#<F< ~D8;YY2		
 	
 .dHgFF	
 	
 	
 $   ;XFFFtt   BHW[\\\tt !GSWSbccct*dk/R6688BFCC   GRRRtt .g666s6   	 $$BB+ +*D&DD4/E$ $*FFc                   t                      }t          |           }t                      }	 |5  t          | ||          cddd           S # 1 swxY w Y   dS # t          $ r! t
                              d| d           Y dS w xY w)a  Fetch account usage for a provider within the active profile context.

    Concurrency is capped by the module-level BoundedSemaphore so that rapid
    UI polls (e.g. Settings page refresh) cannot exhaust file-descriptors or
    memory by spawning more than _MAX_CONCURRENT_ACCOUNT_USAGE_PROBES probe
    subprocesses simultaneously.  Each probe runs up to 35 s.

    A warm worker-pool (reuse of persistent subprocess handles) is a natural
    follow-up if this first slice proves insufficient in production.
    r"  Nz$Failed to fetch account usage for %sTr-  )rh   r   r1   rC  r:   r;  r<  )r   rg   r   sems       r#   )_fetch_account_usage_with_profile_contextrF  N  s     D#H--G
,
.
.C	 	 	6  	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	    ;XPTUUUtts4   A AA AA AA 'BBdisplay_namedict[str, Any]c           
     Z   t          |           }t          |          }|r9|                    d          r$d| |dd|                    d          pdd || dd	S d}|r6t          |                    d          pd                                          }|r| d	| n| d
}d| |ddd ||dS )Nr  Tr  r  z account limits loaded.)	okr   rG  	supportedstatusr   quotaaccount_limitsmessager   r  z! account limits are unavailable. zO account limits are unavailable. Confirm provider authentication and try again.Funavailable)rJ  r   rG  rK  rL  rM  rN  rO  )rF  r  r   r   r   )r   rG  r   rN  reasonrO  s         r#   _provider_account_usage_statusrR  h  s   8BBH6x@@N 
.,,[99 
 (!#''00D4D,&???

 

 
	
 F M^''(<==CDDJJLL 	n<BB&BBBmmm  $(	 	 	r%   c           
     r   | pt                      pd                                                                }|s
ddddddddS t          j        ||                    dd                                                    }|t          v rt          ||          S |d	k    rd
}d||dddd| d| dS t          d	          }|s
dd	|dddddS t          j                            t          d| dd          }	 t          j                            |t                    5 }|                                }ddd           n# 1 swxY w Y   t#          |t$          t&          f          r't)          j        |                    d                    nt)          j        |          }t/          |          }	dd	|ddd|	ddS # t          j        j        $ r+}
|
j        dv rdnd}|dk    rdnd}dd	|d|d|dcY d}
~
S d}
~
wt6          t          j        j        t(          j        t<          t>          f$ r dd	|dddddcY S w xY w)a  Return sanitized quota/rate-limit status for the active provider.

    OpenRouter keeps its documented key endpoint. OAuth-backed account usage
    providers reuse Hermes Agent's /usage account-limits abstraction so WebUI
    stays aligned with CLI/Gateway provider semantics.
    r   FNrP  z!No active provider is configured.)rJ  r   rG  rK  rL  rM  rO  - r=   zcOpenAI/Anthropic rate-limit headers are a follow-up once WebUI captures provider response metadata.unsupportedz"Quota status is not available for z. Tno_keyzMOpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.zBearer zapplication/json)AuthorizationAccept)headers)r)  rk   r  zOpenRouter creditszOpenRouter quota status loaded.)rJ  r   rG  rK  rL  r   rM  rO  )i  i  invalid_keyz+OpenRouter rejected the configured API key.z3OpenRouter quota status is temporarily unavailable.) r   r   r   r	   r   r   r  _ACCOUNT_USAGE_PROVIDERSrR  r   urllibrequestRequest_OPENROUTER_KEY_URLurlopen_PROVIDER_QUOTA_TIMEOUT_SECONDSreadr   bytes	bytearrayr>  r?  decoder   error	HTTPErrorcodeTimeoutErrorURLErrorr@  r   r   )r   r   rG  r   r   reqresprx   r   rM  excrL  rO  s                r#   get_provider_quotaro    sE    :244:AACCIIKKH 	
 #:
 
 	
 %(83C3CC3M3M3S3S3U3UVVL+++-hEEE<v (#TLTTFTT
 
 	
 $L11G 	
$(f
 
 	
 .
 
 0w00(
 
 !  C(
^##C1P#QQ 	UY))++C	 	 	 	 	 	 	 	 	 	 	 	 	 	 	5?eYEW5X5Xm$*SZZ00111^b^hil^m^m*733$(!)8	
 	
 		
 <! 
 
 
"%(j"8"8m && :9F 	 $(
 
 	
 	
 	
 	
 	
 	
 &,/1EwPZ[ 	
 	
 	
$(#L
 
 	
 	
 	
	
sI   7&F: D>2F: >EF: EA3F: :H6 G4.H64?H65H6c                    | t           v S )zACheck whether a provider uses OAuth/token flows (managed by CLI).)_OAUTH_PROVIDERS)r   s    r#   _provider_is_oauthrr    s    ***r%   c                   ) g } t          t          j                              t          t          j                              z  }t	                      }|                    di           }t          |t                    r'|                    |                                           |                    t                     t          |          D ]}t          j        ||                    dd                                                    }t          |          }t          |          }d}d}	|rd}	 ddlm}
  |
|          }t          |t                    r.|                    d	          rd
}|                    dd          }n_|r/d}t          |t                    r|                    d          nd}	n.d}t          |t                    r|                    d          nd}	n# t"          $ r! t$                              d|d
           Y ncw xY w|rt(                              |          }|rt+                      dz  }t-          |          }|                    |          rd}nt/          j        |          rd}nd}t2                              |d          pdD ]7}|                    |          rd}d
} nt/          j        |          rd}d
} n8|sd}nd}n|t(          vrddl}|                    d|          ro	 ddlm}
  |
|          }t          |t                    r7|                    d	          r"d
}|                    dd          }|dv r|nd}d
}n# t"          $ r Y nw xY wt9          t          j        |g                     }t;          |          }|dk    r\t=          d          }t?                      D ]}||vr|                     |           tC          ||          }|r|}t;          |          }|dk    rt	 ddl"m#}  |d          pg }|r3ddl$m%)m&}  ||          \  }})fd|D             }t;          |          }n*# t"          $ r t$                              d           Y nw xY w|dk    r\	 ddl"m#}  |d          pg }|rd |D             }t;          |          }n*# t"          $ r t$                              d            Y nw xY wt          |t                    r|                    |i           }t          |t                    r|d!|v rx|d!         }t          |t                    r"|d" |                                D             z   }n$t          |t8                    r|d# |D             z   }|dk    rt;          |          }|                      |||| o|t(          v |||	||d$	           |                    d%g           } t          | t8                    r| D ]}!t          |!t                    r|!                    d&          s.tO          |!d&                   (                                }"tS          |"          }#|#st$          *                    d'|"           g }$t          |!                    d!          t8                    rd( |!d!         D             }$n'|!                    d)          r|!d)         |!d)         d*g}$tO          |!                    d+          pd          }%tW          |%(                                          }&|%,                    d,          rS|%-                    d-          r>|%d.d/         }tW          t/          j        |d          (                                          }&|                      |#|"|&d|&rdnd|$d0           d}'|                    d)i           }(t          |(t                    r|(                    d1          }'| |'d2S )3a  Return a list of all known providers with their configuration status.

    Each entry contains:
    - ``id``: canonical provider slug
    - ``display_name``: human-readable name
    - ``has_key``: whether an API key is configured
    - ``configurable``: whether the key can be set from the WebUI
    - ``key_source``: where the key was found (``env_file``, ``env_var``,
      ``config_yaml``, ``oauth``, ``none``)
    - ``models``: list of known model IDs for this provider
    r   rT  rU  noneNoauthr   )get_auth_status	logged_inT
key_sourceconfig_yamlrg  Fz#hermes_cli auth check failed for %sr-  r   env_filer   r0   z^[a-z][a-z0-9_-]{0,63}$r   >   r  ru  tokenconfigr&   r`   )provider_model_ids)_format_nous_label_build_nous_featured_setc                2    g | ]}d |  |          dS )z@nous:idr   r0   )r   midr~  s     r#   r   z!get_providers.<locals>.<listcomp>  sC         .~~8J8J38O8OPP  r%   z1Failed to load Nous Portal models from hermes_clirV   c                    g | ]}||d S r  r0   )r   r  s     r#   r   z!get_providers.<locals>.<listcomp>  s     KKKCS377KKKr%   z/Failed to load LM Studio models from hermes_climodelsc                    g | ]}||d S r  r0   r   ks     r#   r   z!get_providers.<locals>.<listcomp>  s     &X&X&Xa!'<'<&X&X&Xr%   c                    g | ]}||d S r  r0   r  s     r#   r   z!get_providers.<locals>.<listcomp>  s     &Q&Q&Qa!'<'<&Q&Q&Qr%   )	r  rG  has_keyconfigurableis_oauthrx  
auth_errorr  models_totalr   r   z6Custom provider entry %r produced empty slug; skippingc                L    g | ]!}t          |          t          |          d "S r  )r   )r   ms     r#   r   z!get_providers.<locals>.<listcomp>  s,    SSSCFFSVV<<SSSr%   r   r  r   r   r   r(   r   )r  rG  r  r  rx  r  r   )r   r   ).setr	   keysr
   r   r   r   r   updaterq  sortedr   r  rr  r   hermes_cli.authrv  r:   r;  r<  r[   rh   r|   r   r   r]   rematchr   lenr   r   r   r   hermes_cli.modelsr}  r  r~  r  r   r   r   warningr   ru   r   )*r   	known_idsr   r   r   rG  r  r  rx  r  _gasrL  r   ri   r   aliasedr   _re_raw_ksr  r  live_idsr  live_models_provider_model_idsr  featured_ids_extras_pmilm_liver   
cfg_modelscustom_providers_cfgr   cp_namecp_id	cp_models
cp_api_key
cp_has_keyr   r   r~  s*                                            @r#   get_providersr    sa	    I %*,,--4D4I4K4K0L0LLI ,,CGGK,,M-&& /++--... %&&&i   w w(,S#++c32G2G2M2M2O2OPP%c**#C(( 

 N	 JXCCCCCCcfd++ 
[

;0G0G 
["G!'L'!B!BJJ [ "/J8B648P8P!ZG!4!4!4VZJJ#G8B648P8P!ZG!4!4!4VZJ X X X BCRVWWWWWX
  4	'++C00G ++--6+H55
>>'** 3!+JJYw'' 3!*JJ $G!:!>!>sB!G!G!M2 " "%>>%00 ")3J&*G!E9U++ ")2J&*G!E" # 3%2
*

))) yy3S99 
GGGGGG!T#YYF!&$// (FJJ{4K4K ("&"(**\2">">07;^0^0^WWdk
#'    D &*333446{{ .  4^DDH:<< ) )h&&OOC(((8hGGK +$"6{{ &==RWWWWWW..v66<" 	1WWWWWWWW,D,DX,N,N)L'   #/  F $'x==L R R RPQQQQQR *PHHHHHH$z**0b /KK7KKKF#&v;;L P P PNOOOOOP mT** 	/(,,S"55L,-- /(l2J2J)(3
j$// R#&X&XjooFWFW&X&X&XXFF
D11 R#&Q&Qj&Q&Q&QQF &==#&v;;L( (LES4E-E $$ )!
 
 	 	 	 	( 77#5r::&--  & 	 	Bb$'' rvvf~~ "V*oo++--G3G<<E L   I"&&**D11 HSSblSSS		 H$&wK"W+FFG	RVVI..4"55Jj..0011J$$T** Bz/B/B3/G/G B$QrT*!")GR"8"8">">"@"@AA
 '% %/9Emmv#      O$$I)T"" 4#--
33 *  sK   3B3G(('HHAM%%
M21M2AQ$Q?>Q?	0R::$S! S!c                   |                                                                  } | sdddS t          |           rddt          j        | |            ddS t
                              |           }|sddt          j        | |            ddS |r9|                                 }d|v sd	|v rdd
dS t          |          dk     rdddS t                      dz  }	 t          |||i           n^# t          $ r}dt          |          dcY d}~S d}~wt          $ r-}t                              d|            dd| dcY d}~S d}~ww xY wt                       d| t          j        | |           |rdnddS )zSet or update the API key for a provider.

    Writes the key to ``~/.hermes/.env`` using the standard env var name.
    If ``api_key`` is None or empty, the key is removed.

    Returns a status dict with the operation result.
    FzProvider ID is required.)rJ  rg  rq   zP' uses OAuth authentication. Use `hermes model` in the terminal to configure it.zCannot configure API key for 'z7'. This provider does not have a known env var mapping.r   r   r      zAPI key appears too short.r   Nz(Failed to write env file for provider %szFailed to save API key: Tupdatedremoved)rJ  r   rG  action)r   r   rr  r	   r   r[   r  rh   r   r   r   r:   r;  	exceptionr   )r   r   r   ri   rn  s        r#   set_provider_keyr    s;    ##%%++--K B&@AAA+&& 
L*.{KHH L L L
 
 	
  ##K00G 
M6G6KKYd6e6e M M M
 
 	
  H--//7??dgoo*XYYYw<<!*FGGG!!F*HH7G"45555 0 0 0c#hh//////// H H HC[QQQ&F&F&FGGGGGGGGH  )-k;GG&5))I	  s0   C1 1
E;DEE"EEEc                n    t          | d          }|                    d          rt          |            |S )aQ  Remove the API key for a provider.

    Removes the key from ``~/.hermes/.env`` (via ``set_provider_key``)
    and also cleans up ``config.yaml`` if the key is stored there
    (``providers.<id>.api_key`` or top-level ``model.api_key`` when this
    provider is the active one).

    Returns a status dict with the operation result.
    NrJ  )r  r   _clean_provider_key_from_config)r   results     r#   remove_provider_keyr  2  s<     k400F
 zz$ 5'444Mr%   c                D   ddl m} 	 ddl m} |                                }n# t          $ r Y dS w xY w|                                sdS 	 ddl}d}|5  |                    d          }|                    |          }t          |t                    s	 ddd           dS |                    di           }t          |t                    rE|                    | i           }	t          |	t                    r|	                    d          r|	d= d	}|                    d
i           }
t          |
t                    rz|
                    d          re|
                    d          }|rNt          |                                                                          |                                 k    r|
d= d	}|                    dg           }t          |t                    rW|D ]T}t          |t                    r=t!          | |                    d                    r|                    d          r|d= d	}U|rt#          ||           ddd           n# 1 swxY w Y   |rt%                       dS dS # t          $ r t&                              d|            Y dS w xY w)u  Remove provider API key entries from config.yaml.

    Handles three storage locations:
    1. ``providers.<id>.api_key`` — per-provider key
    2. ``model.api_key`` — top-level key (only if provider is active)
    3. ``custom_providers[].api_key`` — custom provider entries

    Writes back to config.yaml only if something was actually removed.
    Uses ``_cfg_lock`` to prevent TOCTOU races.
    r   )	_cfg_lockNFrk   rl   r   r   Tr   r   r   r   z4Failed to clean provider key from config.yaml for %s)r  r  r|  _get_config_pathr:   rr   yamlrs   	safe_loadr   r   r   r   r   r   r   r$   r   r   r;  r  )r   r  _configconfig_path_yamlchangedrx   r   r   r   r   r   r   r   s                 r#   r  r  G  s^    %$$$$$	 	%$$$$$..00     .^ !	9 !	9'''99C//#&&Cc4(( 	!	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9  GGK44M-.. #,00bAAlD11 #l6F6Fy6Q6Q #$Y/"G ,,I)T** #y}}Y/G/G #"+--
";";" #s?';';'A'A'C'C'I'I'K'K{O`O`ObOb'b'b!),"G  #ww'92>>*D11 /* / /B!"d++ /8bffVnnUU /!vvi00 /$&yM*. 9&{C888C!	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9 !	9L  	OOOOO	 	 ^ ^ ^OQ\]]]]]]^sP   # 
11I6 AII6 "F)II6 II6 II6 6%JJ)r   r   r   r   r   r   )r   r*   )r   r2   )r   r   )ri   r   r   rZ   )ri   r   r}   r~   r   r2   )r   r   r   r   )r   r   r   r   )r   r   )r{   r   r   r   )r   r   r   r   )r{   r   r   r   )r   r   r   r   )r   r   r  r   r   r   r   r   )rg   r   r   r   r   r   r   rZ   )r   r   r   r   )r   r   rg   r   r   r   r   r   )r   r   r   r   )r   r   rG  r   r   rH  r,   )r   r   r   rH  )r   rH  )r   r   r   r   r   rH  )r   r   r   rH  )r   r   r   r2   )J__doc__
__future__r   r>  loggingr   r8   r2  r0  r-   urllib.errorr]  urllib.requestr   r   pathlibr   typesr   typingr   r  r	   r
   r   r   r   r   r   r   r   r   r   	getLogger__name__r;  r$   r`  rb  r5  	frozensetr\  r/   r8  r)   __annotations__r1   r<   r9  r[   r]   rq  rh   r|   r   r   r   r   r   r   r   r  r  r  r!  rC  rF  rR  ro  rr  r  r  r  r  r0   r%   r#   <module>r     sc     # " " " " "   				      



             ' ' ' ' ' ' ' '       ! ! ! ! ! !                                
	8	$	$
 
 
 
 9 "% ,0 )$9nk%BCC  () $. )$ EI  H H H H* * * *    c" T$%&$%$$% $% 	$%
 $% 
=$% >$% "$%  $% &$% "$% M$% $% *$% ($%0 $1$%D E$%F G$% $%  $ $ $ $^ %9      9      ' ' ' '   "_ _ _ _D2 2 2 2j, , , ,^      	 	 	 	   % % % %P IMdh M M M M M M   >   4 ]a .7 .7 .7 .7 .7 .7b   4! ! ! !H^
 ^
 ^
 ^
 ^
B+ + + +@ @ @ @F7 7 7 7t   *I^ I^ I^ I^ I^ I^r%   