
    *jVS                      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ZddlZddlZddlmZmZ ddlmZmZmZ ddlmZ ddlmZ ddlmZ 	 ddlZn# e$ r dZY nw xY w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,ddZ-dZ.dZ/dZ0dZ1dZ2 e3ddh          Z4dZ5dZ6da7de8d<   i Z9de8d<    ej:                    Z;ddZ<dd!Z=d"Z>i d#d$dd%d&d'd(d)d*d+d,d-d.d/d0d1d2d3d4d5d6d7d8d9d:d;d<d=d>d?d@dAdBdCdDdEiZ?dFe8dG<   dBdHiZ@dIe8dJ<    e3h dK          ZAddMZBddOZCddRZDddTZEddUZFddVZGddYZHddZZIdd\ZJdd]ZKdd`ZLddcZMdddZNddgZOdddhddlZPddnZQddoZRddqZSddtZTddduZUddvZVddwddxZWdydzdd|ZXdydzdd~ZYddydzddZZddZ[dZ\dZ] ej:                    Z^ddZ_edd            Z`ddZaddZbddZcddZdddZedddZfddZgddZhddZiddZjd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)contextmanagernullcontext)datetime	timedelta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. Fcustom:)r   striplowerr   add)r   r   pidraw_nameslug
candidatess         0/Users/bertmahoney/hermes-webui/api/providers.py_custom_provider_name_matchesr(   2   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@g     F@@   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_semaphorez-dict[tuple[str, str, str], tuple[float, Any]]_account_usage_status_cache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_semaphorer7   {   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)r;   libcs     r'   _account_usage_preexec_fnrB      s_    {{4  

1fn%%%%%   s   9= 
A
Aa@  
import base64
import json
import sys
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, 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"
_CODEX_POOL_USAGE_TIMEOUT_SECONDS = 4.0
_CODEX_POOL_MAX_WORKERS = 6


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),
        })
    payload = {
        "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)),
    }
    pool = getattr(snapshot, "pool", None)
    if isinstance(pool, dict):
        payload["pool"] = pool
    return payload


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 _snapshot_windows_payload(snapshot):
    windows = []
    for window in getattr(snapshot, "windows", ()) or ():
        label = str(getattr(window, "label", "") or "").strip()
        if not label:
            continue
        used_percent = _number(getattr(window, "used_percent", None))
        remaining_percent = None
        if used_percent is not None:
            remaining_percent = max(0.0, min(100.0, 100.0 - float(used_percent)))
        windows.append({
            "label": label,
            "used_percent": used_percent,
            "remaining_percent": remaining_percent,
            "reset_at": _iso(getattr(window, "reset_at", None)),
            "detail": getattr(window, "detail", None),
        })
    return windows


def _snapshot_details_payload(snapshot):
    return [
        str(detail).strip()
        for detail in (getattr(snapshot, "details", ()) or ())
        if str(detail).strip()
    ]


def _safe_entry_label(entry, index):
    label = _entry_value(entry, "label", "source") or ""
    if not label:
        label = "Credential " + str(index)
    label = " ".join(str(label).split())
    if len(label) > 64:
        label = label[:61].rstrip() + "..."
    return label


def _safe_unavailable_reason(reason):
    text = " ".join(str(reason or "").split())
    if not text:
        return None
    lowered = text.lower()
    sensitive_terms = ("access_token", "refresh_token", "authorization", "bearer ", "jwt", "secret")
    if any(term in lowered for term in sensitive_terms):
        return "Usage unavailable for this credential."
    return text[:180]


def _entry_exhausted_ttl_seconds(error_code):
    code = str(error_code or "").strip()
    if code == "401":
        return 5 * 60
    return 60 * 60


def _entry_pool_exhausted_until(entry):
    if str(_entry_value(entry, "last_status") or "").strip().lower() != "exhausted":
        return None
    reset_at = _parse_dt(getattr(entry, "last_error_reset_at", None))
    if reset_at is not None:
        return reset_at
    status_at = _parse_dt(getattr(entry, "last_status_at", None))
    if status_at is None:
        return None
    return status_at + timedelta(seconds=_entry_exhausted_ttl_seconds(_entry_value(entry, "last_error_code")))


def _entry_is_pool_exhausted(entry):
    exhausted_until = _entry_pool_exhausted_until(entry)
    return exhausted_until is not None and datetime.now(timezone.utc) < exhausted_until


def _entry_pool_exhausted_reason(entry):
    code = _entry_value(entry, "last_error_code")
    reset_at = _entry_pool_retry_after(entry)
    reason = "Credential pool marked this credential exhausted"
    if code:
        reason += " after provider status " + code
    if reset_at:
        reason += "; retry after " + reset_at
    return reason + "."


def _entry_pool_retry_after(entry):
    return _iso(_entry_pool_exhausted_until(entry))


def _fetch_codex_entry_snapshot(entry):
    access_token = _entry_value(entry, "runtime_api_key", "access_token")
    if not access_token:
        return None, False, "No runtime token available."
    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=_CODEX_POOL_USAGE_TIMEOUT_SECONDS) as response:
        payload = json.loads(response.read().decode("utf-8") or "{}")
    return _codex_snapshot_from_usage_payload(payload), True, None


def _best_remaining_by_window(rows):
    best = {}
    for row in rows:
        if row.get("status") != "available":
            continue
        label = row.get("label") or "Credential"
        for window in row.get("windows") or []:
            if not isinstance(window, dict):
                continue
            window_label = str(window.get("label") or "").strip()
            remaining = _number(window.get("remaining_percent"))
            if not window_label or remaining is None:
                continue
            candidate = {
                "label": window_label,
                "remaining_percent": remaining,
                "used_percent": window.get("used_percent"),
                "reset_at": window.get("reset_at"),
                "detail": window.get("detail"),
                "credential_label": label,
            }
            current = best.get(window_label.lower())
            # The normalized Codex account-limit payload currently exposes
            # percentages, not absolute request/token capacity. If absolute
            # remaining capacity becomes available, prefer it here.
            if current is None or float(remaining) > float(current.get("remaining_percent") or -1):
                best[window_label.lower()] = candidate
    return list(best.values())


def _next_reset_at(rows):
    best_dt = None
    best_text = None
    for row in rows:
        for window in row.get("windows") or []:
            if not isinstance(window, dict):
                continue
            reset_text = window.get("reset_at")
            dt = _parse_dt(reset_text)
            if dt is None:
                continue
            if best_dt is None or dt < best_dt:
                best_dt = dt
                best_text = _iso(dt)
    return best_text


def _codex_pool_snapshot(entries, rows, queried):
    available_rows = [row for row in rows if row.get("status") == "available"]
    exhausted_rows = [row for row in rows if row.get("status") == "exhausted"]
    failed_rows = [row for row in rows if row.get("status") not in {"available", "exhausted"}]
    plans = []
    for row in rows:
        plan = row.get("plan")
        if plan and plan not in plans:
            plans.append(plan)
    best_windows = _best_remaining_by_window(rows)
    pool = {
        "total_credentials": len(entries),
        "queried_credentials": queried,
        "available_credentials": len(available_rows),
        "exhausted_credentials": len(exhausted_rows),
        "failed_credentials": len(failed_rows),
        "plans": plans,
        "next_reset_at": _next_reset_at(rows),
        "best_remaining_by_window": best_windows,
        "credentials": rows,
    }
    details = [str(len(available_rows)) + "/" + str(len(entries)) + " credentials available"]
    if exhausted_rows:
        details.append(str(len(exhausted_rows)) + " exhausted")
    if failed_rows:
        details.append(str(len(failed_rows)) + " failed to load")
    if plans:
        details.append("Plans: " + ", ".join(plans))
    plan = plans[0] if len(plans) == 1 else None
    windows = tuple(
        SimpleNamespace(
            label=window.get("label"),
            used_percent=window.get("used_percent"),
            reset_at=window.get("reset_at"),
            detail="Best of " + str(len(available_rows)) + " available credentials",
        )
        for window in best_windows
    )
    return SimpleNamespace(
        provider="openai-codex",
        source="usage_api_pool",
        title="Account limits",
        plan=plan,
        windows=windows,
        details=tuple(details),
        available=bool(available_rows),
        unavailable_reason=None if available_rows else "No Codex pool credentials returned available account limits.",
        fetched_at=datetime.now(timezone.utc),
        pool=pool,
    )


def _codex_pool_exhausted_row(entry, index):
    label = _safe_entry_label(entry, index)
    retry_after = _entry_pool_retry_after(entry)
    return {
        "label": label,
        "status": "exhausted",
        "plan": None,
        "windows": [],
        "details": [],
        "unavailable_reason": _entry_pool_exhausted_reason(entry),
        "retry_after": retry_after,
        "fetched_at": None,
    }


def _probe_codex_pool_entry(item):
    index, entry = item
    label = _safe_entry_label(entry, index)
    did_query_count = 0
    try:
        snapshot, did_query, reason = _fetch_codex_entry_snapshot(entry)
        if did_query:
            did_query_count = 1
    except Exception as exc:
        snapshot = None
        reason = str(exc)
    windows = _snapshot_windows_payload(snapshot) if snapshot is not None else []
    details = _snapshot_details_payload(snapshot) if snapshot is not None else []
    snapshot_available = _snapshot_available(snapshot)
    status = "available" if snapshot_available else "unavailable"
    row = {
        "label": label,
        "status": status,
        "plan": getattr(snapshot, "plan", None) if snapshot is not None else None,
        "windows": windows,
        "details": details,
        "unavailable_reason": None if snapshot_available else _safe_unavailable_reason(reason or getattr(snapshot, "unavailable_reason", None)),
        "fetched_at": _iso(getattr(snapshot, "fetched_at", None)) if snapshot is not None else None,
    }
    return index, row, did_query_count


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

        pool = load_pool("openai-codex")
        entries = list(pool.entries()) if pool is not None and hasattr(pool, "entries") else []
        if not entries:
            return None
        rows_by_index = {}
        probe_items = []
        queried = 0
        for index, entry in enumerate(entries, start=1):
            if _entry_is_pool_exhausted(entry):
                rows_by_index[index] = _codex_pool_exhausted_row(entry, index)
            else:
                probe_items.append((index, entry))
        if probe_items:
            max_workers = min(_CODEX_POOL_MAX_WORKERS, len(probe_items))
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                for index, row, did_query_count in executor.map(_probe_codex_pool_entry, probe_items):
                    rows_by_index[index] = row
                    queried += did_query_count
        rows = [rows_by_index[index] for index in range(1, len(entries) + 1)]
        return _codex_pool_snapshot(entries, rows, queried)
    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":
    pool_snapshot = _fetch_codex_account_usage_from_pool()
    if isinstance(getattr(pool_snapshot, "pool", None), dict):
        snapshot = pool_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copilot	xai-oauthr+   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.profilesrk   ImportErrorr	   homerj   s    r'   _get_hermes_homero     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#=r:   "')exists	read_text
splitlinesr    
startswithsplitr@   )rp   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-tokendict[str, Any]c                   t          | t                    r|                     d          dk    ri S |                     dd          d         }|ddt	          |          dz  z
  dz  z  z  }	 t          j        t          j        |	                    d                    
                    d                    }n# t          $ r i cY S w xY wt          |t                    r|ni S )a!  Decode JWT claims for token-shape classification only.

    The signature is intentionally not verified because this helper is not an
    authorization decision: it only prevents a Codex OAuth JWT-shaped value from
    being treated as a raw OpenAI API key in provider-card detection.
    .r-   r:   rv      rr   )
isinstancer   countr}   lenjsonloadsbase64urlsafe_b64decodeencodedecoder@   dict)r   payloadclaimss      r'   _decode_jwt_claims_unverifiedr     s     eS!! U[[%5%5%:%:	kk#q!!!$Gsq3w<<!++q011GF4W^^G5L5LMMTTU\]]^^   			--56625s   ,AB9 9CCr   c                4   t          | pd                                          }|r|                    d          rdS t          |          sdS                     d          }t          |t                    r|rdS t          fddD                       S )zMReturn True when a value is a ChatGPT/Codex OAuth JWT, not an OpenAI API key.r   zsk-Fzhttps://api.openai.com/authTc              3      K   | ]}|v V  	d S r2   r6   ).0r   r   s     r'   	<genexpr>z0_looks_like_codex_oauth_token.<locals>.<genexpr>  s'      aasf}aaaaaar)   )chatgpt_account_idzhttps://api.openai.com/profile)r   r    r|   r   getr   r   any)r   r   
auth_claimr   s      @r'   _looks_like_codex_oauth_tokenr     s    ""$$E E$$U++ u*511F u9::J*d## 
 taaaa(`aaaaaar)   c                    t          |pd                                          }|sdS | pd                                                                dk    rt          |          rdS dS )Nr   FrF   T)r   r    r!   r   )r   r   texts      r'   !_provider_value_counts_as_api_keyr     sm    u{!!##D ur  ""((**h66;XY];^;^6u4r)   c                   | pd                                                                 dk    rdS g }t                              |           }|rt	                      dz  }t          |          }|                    |                    |                     |                    t          j        |                     t                              | d          pdD ]Q}|                    |                    |                     |                    t          j        |                     Rt                      }|                    di           }t          |t                    rvt          |                    d          pd                                                                           }|| k    r(|                    |                    d                     |                    d	i           }	t          |	t                    rS|	                    | i           }
t          |
t                    r(|                    |
                    d                     |                    d
g           }t          |t                    r|D ]}t          |t                    rt          | |                    d                    r|                    d          }t          |t                    rZ|                    d          rE|                    d          r0|                    t          j        |dd                              |                    |           t%          d |D                       S )a  True when the bare OpenAI credential slot contains only a Codex OAuth JWT.

    Users who authenticate Codex can end up with a ChatGPT/Codex JWT in a
    legacy OPENAI_API_KEY-shaped location. That value should not make the bare
    OpenAI API provider appear as configured, and the Providers tab should not
    show an extra OpenAI card solely because of that Codex-only credential.
    r   rF   F.envr6   modelproviderapi_key	providerscustom_providersr   ${}r-   c              3  R   K   | ]"}t          t          |pd                     V  #dS )r   N)r   r   )r   r   s     r'   r   z;_provider_has_shadowed_codex_oauth_value.<locals>.<genexpr>)  s7      SS5,S"-=-=>>SSSSSSr)   )r    r!   ra   r   ro   r   appendosgetenvrc   r   r   r   r   listr(   r|   endswithr   )r   r~   env_varrp   
env_valuesaliascfg	model_cfgactive_providerproviders_cfgprovider_cfgr   cpcp_keys                 r'   (_provider_has_shadowed_codex_oauth_valuer      s    	r  ""((**h66uF##K00G ,#%%.#H--
jnnW--...bi(())).22;CCIr 	, 	,EMM*..//000MM")E**++++
,,C$$I)T"" 4immJ77=2>>DDFFLLNNk))MM)--	22333GGK,,M-&& 7$((b99lD)) 	7MM,**955666ww1266"D)) *" 	* 	*B"d## *(EkSUSYSYZ`SaSa(b(b *	**fc** *v/@/@/F/F *6??[^K_K_ *MM")F1R4L"9"9::::MM&)))SSFSSSSSSr)   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_LOCKNrr   rs   ru   rv   r:   
,API key must not contain newline characters.c                    g | ]}||S r2   r6   )r   ls     r'   
<listcomp>z#_write_env_file.<locals>.<listcomp>e  s    AAAa1====r)   r   r   Tparentsexist_okz.env_.tmpdirprefixsuffixw)%api.streamingr   statry   rz   r{   r@   	enumerater    r|   r}   r   itemsr   environpopr   
ValueErrorr   extendparentmkdirjoinS_IRUSRS_IWUSRtempfilemkstempfdopenwriteflushfsyncfilenochmodreplaceBaseExceptionunlinkOSError)rp   r   r   _statexisting_linesexisting_key_indices_i_raw	_stripped_existing_key_indices_keyoutput_linesnew_keysr   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                T   t                               |           }|rt                      dz  }t          |          }|                    |          }t	          | |          rdS t          j        |          }t	          | |          rdS t                              | d          pdD ]M}t	          | |                    |                    r dS t	          | t          j        |                    r dS Nt                      }|                    di           }t          |t                    rt          |                    d          pd                                          r|                    d          }	|	rnt          |	                                                                          |                                 k    r%t	          | |                    d                    rdS |                    di           }
t          |
t                    r|
                    | i           }t          |t                    r[t          |                    d          pd                                          r%t	          | |                    d                    rdS |                    d	g           }t          |t                    rc|D ]`}t          |t                    rIt          | |                    d
                    r&t	          | |                    d                    r dS ad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)
    r   Tr6   r   r   r   r   r   r   r   F)ra   r   ro   r   r   r   r   rc   r   r   r   r   r    r!   r   r(   )r   r   rp   r   env_file_value	env_valuer   r   r   r   r   r   r   r   s                 r'   _provider_has_keyr     s     ##K00G #%%.#H--
#00,[.II 	4Ig&&	,[)DD 	4 /22;CCIr 	 	E0jnnU>S>STT tt0bi>N>NOO 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0immI>V>VWW tGGK,,M-&& $((b99lD)) 	c,2B2B92M2M2SQS.T.T.Z.Z.\.\ 	0l>N>Ny>Y>YZZ tww1266"D)) $" 	$ 	$B"d## $0bffVnnMM $8bffYFWFWXX $#tt5r)   
str | Nonec                R   | pd                                                                 } t                              |           }|rbt	                      dz  }t          |          }|                    |          }t          | |          r#t          |                                           pdS t          j	        |          }t          | |          r#t          |                                           pdS t                              | d          pdD ]}|                    |          }t          | |          r%t          |                                           pdc S t          j	        |          }t          | |          r%t          |                                           pdc S t                      }	|	                    di           }
t          |
t                    rt          |
                    d          pd                                                                           }t          |
                    d          pd                                           }|r|| k    rt          | |          r|S |	                    di           }t          |t                    rs|                    | i           }t          |t                    rHt          |                    d          pd                                           }t          | |          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 t          | |          r|c S dS )zDReturn a configured provider API key without exposing it to callers.r   r   Nr6   r   r   r   r   r   r   r   r   r-   r   )r    r!   ra   r   ro   r   r   r   r   r   rc   r   r   r   r   r(   r|   r   )r   r   rp   r   r   r   r   alias_file_valuealias_valuer   r   r   	model_keyr   r   provider_keyr   r   r   s                      r'   _get_provider_api_keyr    s   $"++--3355K##K00G 8#%%.#H--
#00,[.II 	7~&&,,..6$6Ig&&	,[)DD 	2y>>''))1T1.22;CCIr 	8 	8E)~~e440>NOO =+,,2244<<<<)E**K0kJJ 8;''--//747778 ,,C$$I)T"" immJ77=2>>DDFFLLNN	i006B77==??	 	K77<]^ikt<u<u7GGK,,M-&& $$((b99lD)) 	$|//	::@bAAGGIIL0lKK $##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4[&II "!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   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r2   )	r   r   intfloatr   r    
is_integer	TypeErrorr   )r   r   numbers      r'   _quota_numberr    s    % %-t%#u&& 5zz!! 	4t$//11=s6{{{v=z"   tts   #B 3B B'&B'r   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                R   | 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	}	t          | dd           }
t          |
t                    r|
|	d<   |	S )Nwindowsr6   labelr   used_percent        g      Y@reset_atdetail)r"  r#  remaining_percentr%  r&  c                    g | ]D}t          |                                          #t          |                                          ES r6   )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+  r0  pool)getattrr   r    r  maxminr
  r   r  r   r   r   )r  r!  windowr"  r#  r'  r)  r*  r+  resultr1  s              r'   !_serialize_account_usage_snapshotr7  (  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
 
F 8VT**D$ vMr)   base_urlr   r   r9  r   c               *    ddl m}  || ||          S )Nr   )fetch_account_usager8  )agent.account_usager;  )r   r9  r   r;  s       r'   _agent_fetch_account_usager=  T  s,    777777x(GLLLLr)   rn   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   ra   r   r    r!   
api.configr@  r@   r   pathsepr   )
rn   r   r   envr   r   r   r@  pythonpath_partsexisting_pythonpaths
             r'   _account_usage_subprocess_envrG  Z  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          t          |                     d          t                    r|                     d          n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   r5  s     r'   r   z5_account_usage_payload_to_snapshot.<locals>.<genexpr>|  s       	 	 fd##	**W%%N33ZZ
++::h''		
 	
 	
	 	 	 	 	 	r)   r!  r6   r   r,  r-  r*  r)  r/  r+  r0  r1  )
r   r,  r-  r*  r!  r)  r/  r+  r0  r1  )r   r   tupler   r
   r   )r   r!  s     r'   "_account_usage_payload_to_snapshotrK  y  s0   gt$$ t 	 	 {{9--3	 	 	 	 	G Z(({{8$$kk'""[[  gkk),,233w{{;//00";;';<<;;|,,$.w{{6/B/BD$I$ISW[[   t   r)   tuple[str, str, str]c                   d}|r:t          j        |                    dd                                                    }| pd                                                                t          t          |                    |fS )Nr   rr   ignore)hashlibsha256r   	hexdigestr    r!   r   r	   )r   rn   r   key_fingerprints       r'   _account_usage_cache_keyrS    sn    O X!.)J)JKKUUWW^""$$**,,c$t**ooOOr)   	cache_keytuple[bool, Any]c                :   t          j                    }t          5  t                              |           }|	 d d d            dS |\  }}||z
  t
          k    rd|fcd d d            S t                              | d            d d d            n# 1 swxY w Y   dS )N)FNT)time	monotonic _account_usage_status_cache_lockr/   r    _ACCOUNT_USAGE_CACHE_TTL_SECONDSr   )rT  nowcachedr0  r  s        r'   _get_cached_account_usager]    s   
.

C	) 9 9,00;;>9 9 9 9 9 9 9 9  &
H???>9 9 9 9 9 9 9 9 	$''	48889 9 9 9 9 9 9 9 9 9 9 9 9 9 9 ;s   BB)BBBc                   t          | pd                                                                          }t          5  |s't                                           	 d d d            d S t          t                    D ])}|d         |k    rt                              |d            *	 d d d            d S # 1 swxY w Y   d S )Nr   r   )r   r    r!   rY  r/   clearr   r   )r   
normalizedr   s      r'   %invalidate_account_usage_status_cachera    s    [&B''--//5577J	) ; ; 	'--///; ; ; ; ; ; ; ; 344 	; 	;C1v##+//T:::	;	; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;s   B3'>B33B7:B7c                   t          j                    t          5  |[t                              |           }||d         	 d d d            d S t                              | d            	 d d d            d S |ft          | <   fdt                                          D             }|D ]}t                              |d            t          t                    t          k    rOt          t          d           }t                              |d            t          t                    t          k    Od d d            d S # 1 swxY w Y   d S )Nr:   c                <    g | ]\  }\  }}|z
  t           k    |S r6   )rZ  )r   r   r0  	_snapshotr[  s       r'   r   z-_set_cached_account_usage.<locals>.<listcomp>  s=     
 
 
0C0*iZ"BBB BBBr)   c                (    t           |          d         S )Nr   )r/   r   s    r'   <lambda>z+_set_cached_account_usage.<locals>.<lambda>  s     ;C @ C r)   rf  )
rW  rX  rY  r/   r   r   r   r    _ACCOUNT_USAGE_CACHE_MAX_ENTRIESr4  )rT  r  r\  expiredr   
oldest_keyr[  s         @r'   _set_cached_account_usagerk    s    .

C	) > >044Y??F!fQi&;	> > > > > > > >
 (++It<<<> > > > > > > > 36x#I.
 
 
 
4O4U4U4W4W
 
 
  	7 	7C'++C6666-..1QQQ+CC  J (++J=== -..1QQQ> > > > > > > > > > > > > > > > > >s   (EE:B=EEE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   rD  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)rB  rn  r@   sys
executable
subprocessDEVNULLPIPE)_ACCOUNT_USAGE_SUBPROCESS_TIMEOUT_SECONDShasattrr   rB   run(_ACCOUNT_USAGE_PARENT_DEATHSIG_BOOTSTRAP_ACCOUNT_USAGE_SUBPROCESS_CODErG  TimeoutExpiredloggerdebug
returncoder   r   rq  r    JSONDecodeErrorrK  )r   rn   r   rn  kwargsprocr   s          r'   #_agent_fetch_account_usage_for_homer    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Frefreshr  c                  t                      }t          |           }t          | ||          }|st          |          \  }}|r|S t	                      }	 |5  t          | ||          }t          ||           |cddd           S # 1 swxY w Y   dS # t          $ r1 t          	                    d| d           t          |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.
    rl  Nz$Failed to fetch account usage for %sTrw  )
ro   r  rS  r]  r7   r  rk  r@   r  r  )	r   r  rn   r   rT  	cache_hitr\  semr  s	            r'   )_fetch_account_usage_with_profile_contextr    sN    D#H--G(4AAI 5i@@	6 	M
,
.
.C 	 	:  H
 &i:::	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	    ;XPTUUU!)T222tts6   B $B	<B 	BB BB 7CCdisplay_namec          
     ^   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  r/  Tr-  r.  z account limits loaded.)	okr   r  	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)r  r   r  r  r  r  r  r  )r  r7  r   r   r    )r   r  r  r  r  reasonr  s          r'   _provider_account_usage_statusr  !  s   87SSSH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          
     v   | 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   FNr  z!No active provider is configured.)r  r   r  r  r  r  r  - r  rC   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.Bearer application/jsonAuthorizationAcceptheadersrs  rr   r/  OpenRouter creditszOpenRouter quota status loaded.)r  r   r  r  r  r"  r  r  )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_PROVIDERSr  r  urllibrequestRequest_OPENROUTER_KEY_URLurlopen_PROVIDER_QUOTA_TIMEOUT_SECONDSreadr   bytes	bytearrayr   r   r   r  error	HTTPErrorcodeTimeoutErrorURLErrorr  r   r   )r   r  r   r  r&  r   reqrespr   r   r  excr  r  s                 r'   get_provider_quotar  E  sJ    :244:AACCIIKKH 	
 #:
 
 	
 %(83C3CC3M3M3S3S3U3UVVL+++-hgVVVV<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   9&F< E 4F<  EF< EA3F< <H8 G60H86?H87H8c                    | t           v S )zACheck whether a provider uses OAuth/token flows (managed by CLI).)_OAUTH_PROVIDERS)r   s    r'   _provider_is_oauthr    s    ***r)   zcost-snapshotsim  c                 .    t                      t          z  S )zReturn the directory for cost-snapshot JSON files.

    Uses the Hermes home directory (profile-aware) so snapshots are
    isolated per profile, matching the existing STATE_DIR convention.
    )ro   _COST_SNAPSHOTS_DIR_NAMEr6   r)   r'   _cost_snapshots_dirr    s      888r)   c              #  p  K   t           ,t                      5  dV  ddd           n# 1 swxY w Y   dS t                      }|                    dd           ||  dz  }|                    dd          5 }t          j        |t           j                   dV  ddd           dS # 1 swxY w Y   dS )zBSerialize cost snapshot read-modify-write across worker processes.NTr   z.lockarr   rs   )fcntlr   r  r   openflockLOCK_EX)r   snap_dir	lock_path	lock_files       r'   _cost_snapshot_file_lockr    sB      }]] 	 	EEE	 	 	 	 	 	 	 	 	 	 	 	 	 	 	"$$HNN4$N///h----I	g	.	. )Iu}---                 s   )--:$B++B/2B/c                N   t           j                            t          d|  dd          }	 t           j                            |t
                    5 }|                                }ddd           n# 1 swxY w Y   t          j        t          |t          t          f          r|                    d          n|          }t          |          }d}t          |t                    rc|                    d|          }t          |t                    r8t!          |                    d	          pd
                                          pd}|                    d          |                    d          |dS # t$          $ r  t&                              dd           Y dS w xY w)zFetch current usage/limit from the OpenRouter ``/auth/key`` endpoint.

    Returns a dict with ``usage``, ``limit``, ``label`` on success, or
    ``None`` on any failure.  Never raises; callers handle the None case.
    r  r  r  r  r  Nrr   r  r"  r   r  r  )r  r  r"  z2OpenRouter key usage fetch failed for cost-historyTrw  )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   	sanitizedr"  r  s           r'   _fetch_openrouter_key_usager    s    .
 
 0w00(
 
 !  C^##C1P#QQ 	UY))++C	 	 	 	 	 	 	 	 	 	 	 	 	 	 	*JsUIDV4W4W`SZZ000]`aa.w77	gt$$ 	E;;vw//D$%% EDHHW--344::<<D]]7++]]7++
 
 	

    ITXYYYtts6   &E: A5)E: 5A99E: <A9=C<E: :&F$#F$list[dict[str, Any]]c           
        t                      |  dz  }|                                sg S 	 |                    d          }t          j        |          }n$# t
          t          j        t          f$ r g cY S w xY wt          |t                    sg S |
                    d          }t          |t                    sg S g }|D ]}t          |t                    st          |
                    d          pd                                          }|sQ|                    |t          |
                    d                    t          |
                    d                    d	           |                    d
            |S )zRead persisted daily snapshots for *provider* from disk.

    Returns a list of ``{date, used, limit}`` dicts sorted by date
    ascending.  Returns an empty list if the file does not exist or is
    corrupt.
    .jsonrr   rs   	snapshotsdater   usedr  r  r  r  c                    | d         S Nr  r6   es    r'   rg  z&_read_cost_snapshots.<locals>.<lambda>  s
    QvY r)   rf  )r  ry   rz   r   r   r   r  r   r   r   r   r   r   r    r   r  sort)r   pathr   r  r  validentryr  s           r'   _read_cost_snapshotsr    s      h#5#5#55D;;== 	nngn..z#T):6   			dD!! 	%%Ii&& 	E 
 
%&& 	599V$$*++1133 	!%))F"3"344"599W#5#566
 
 	 	 	 	
 
JJ&&J'''Ls   *A A87A8r  c                   t                      }|                    dd           ||  dz  }| |d}t          j        |dd          }dd	l}|                    t          |          d
|  dd          \  }}	 t          j        |dd          5 }	|		                    |           |	
                                 t          j        |	                                           d	d	d	           n# 1 swxY w Y   t          j        ||           d	S # t          $ r( 	 t          j        |           n# t           $ r Y nw xY w w xY w)z:Persist daily snapshots for *provider* to disk atomically.Tr   r  )r   r  Fr-   )ensure_asciiindentr   Nr   _r   r   r   rr   rs   )r  r   r   dumpsr   r   r   r   r   r   r   r   r   r   r   r   r   )
r   r  r  r  r   bodyr   r   r   r   s
             r'   _write_cost_snapshotsr    s   "$$HNN4$N///((((D#)<<G:gE!<<<D    "**MM/h///& +  GYYwg666 	""HHTNNNHHJJJHRYY[[!!!	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	
9d#####   	Ii     	 	 	D	sU   ;D AC."D .C22D 5C26D 
ED0/E0
D=:E<D==Er  r  c                H   t          j        t          j                                      d          }t
          5  t          |           5  t          |           }d}|D ]}|d         |k    r||d<   ||d<   d} n|s|                    |||d           |	                    d 	           t          |          t          k    r|t           d
         }t          | |           |cd
d
d
           cd
d
d
           S # 1 swxY w Y   	 d
d
d
           d
S # 1 swxY w Y   d
S )zAppend today's snapshot and return the updated list.

    If a snapshot for today already exists it is updated in-place so
    repeated calls within the same day are idempotent.
    z%Y-%m-%dFr  r  r  Tr  c                    | d         S r  r6   r  s    r'   rg  z'_append_cost_snapshot.<locals>.<lambda>D  s
    6 r)   rf  N)r   r[  r   r  strftime_COST_SNAPSHOT_LOCKr  r  r   r  r   _COST_SNAPSHOT_MAX_DAYSr  )r   r  r  todayr  updatedr  s          r'   _append_cost_snapshotr  *  s    L&&//
;;E 
  %h// 	 	,X66IG"  =E))$)E&M%*E'N"GE	 *
  Q  %!O!OPPPNN22N3339~~ 777%'>&>&?&?@	!(I666#	 	 	 	 	 	 	       	 	 	 	 	 	 	 	 	                 s6   D	BC>%D>D	DD	DDDwindow_daysr	  c                R   t          |           |k    r| | d         nt          |           }g }t          |          D ]\  }}d}|dk    r|                    d          ||dz
                               d          ut	          |d                   t	          ||dz
           d                   z
  }|dk     rt	          |d                   }t          |          dk     rd}nt          |d          }|                    |d         |                    d          |d	           |S )
a  Compute daily deltas from cumulative usage snapshots.

    Each snapshot carries cumulative ``used``; the delta for a day is
    the difference between that day's cumulative value and the previous
    day's.  The oldest day in the window has ``delta=None`` (no
    previous baseline).  If the cumulative value drops, treat that day
    as the start of a fresh series (for example after an API-key rotation)
    and use the current value as that day's delta instead of emitting a
    negative spend bar.
    Nr   r  r:   g&.>r$     r  )r  r  delta)r   r   r   r   r
  absroundr   )r  r  r5  r6  ir  r  s          r'   _compute_deltasr  L  s9    *-Y+)E)EY|}}%%4PY??F#%Ff%%  5q55UYYv&&2va!e}7H7H7P7P7\%-((5Av1F+G+GGEqyyeFm,,5zzD  eQ&MIIf%%
 
 	 	 	 	
 Mr)      daysc                   | pd                                                                 }|sdddddS |dk    rHt          j        ||                    dd	                                                    }d||dd
d| ddS t          j        dd          }t          d          }|s	dd|ddddS t          |          }|,t          d          }t          ||          }dd|dd||dddd
S 	 t          d|d         |d                   }n;# t          $ r. t                              dd           t          d          }Y nw xY wt          ||          }dd|dd|||                    d          |                    d          pddd
S )u  Return daily cost-history snapshots with deltas for a provider.

    Currently only ``openrouter`` is supported.  On each call the
    endpoint fetches the current cumulative usage from the OpenRouter
    ``/auth/key`` endpoint, appends/updates today's snapshot, and
    returns the last *days* snapshots with per-day deltas.

    Returns a dict matching the existing API style (``ok``, ``provider``,
    ``status``, ``message``, …).
    r   FNmissing_providerz9Provider parameter is required.  Use ?provider=openrouter)r  r   r  r  rC   r  r  r  z"Cost history is not available for z/. Only openrouter is supported in this release.)r  r   r  r  r  r  
OpenRouterTr  zMOpenRouter cost history needs an OPENROUTER_API_KEY configured on the server.r  zLOpenRouter cost history is temporarily unavailable. Showing last known data.)
r  r   r  r  r  r  r  r  r"  r  r  r  z.Failed to persist cost snapshot for openrouterrw  r/  r"  r  zOpenRouter cost history loaded.)r    r!   r   r   r   r-  r  r  r  r  r  r@   r  r  )r   r  r   r  r   key_infor  deltass           r'   get_provider_cost_historyr   m  s    !r((**0022H 
(R	
 
 	
 <(,Xx7G7GS7Q7Q7W7W7Y7YZZ (#yLyyy
 
 	
 %(|DDL#L11G 
$(f
 
 	
 +733H )66	 D11$(#e
 
 	
7),8I8T[K\]]		 7 7 7EPTUUU(66			7 Y--F $g&&g&&>*>4  s   -D 5EEc                 R  )* 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 nw xY w|rt(                              |          }|rt+                      dz  }t-          |          }t/          ||                    |                    rd}n3t/          |t1          j        |                    rd}nd}t4                              |d          pdD ]S}t/          ||                    |                    rd}d
} n)t/          |t1          j        |                    rd}d
} nT|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|dk    r|st;          |          rt=          t          j        |g                     }t?          |          }|dk    r\tA          d          }tC                      D ]}||vr|"                    |           tG          ||          }|r|}t?          |          }|dk    r0tG          |tA          d                    }|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          |t<                    r|d% |D             z   }|dk    rt?          |          }| "                    |||| o|t(          v |||	||d&	           |                    d'g           } t          | t<                    r| D ]}!t          |!t                    r|!                    d(          s.tS          |!d(                   *                                }"tW          |"          }#|#st$          ,                    d)|"           g }$t          |!                    d#          t<                    rd* |!d#         D             }$n'|!                    d+          r|!d+         |!d+         d,g}$tS          |!                    d-          pd          }%t[          |%*                                          }&|%.                    d.          rS|%/                    d/          r>|%d0d1         }t[          t1          j        |d          *                                          }&| "                    |#|"|&dd
|&rdnd|$t?          |$          d2           d*|                    d+i           }'t          |'t                    r|'                    d3          **fd4}(| 0                    |(5           | *d6S )7a  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   r  r  noneNoauthr   )get_auth_status	logged_inT
key_sourceconfig_yamlr  Fz#hermes_cli auth check failed for %srw  r   env_filer   r6   z^[a-z][a-z0-9_-]{0,63}$r   >   rD  r  r   configrF   r+   rh   rf   )provider_model_ids)_format_nous_label_build_nous_featured_setc                2    g | ]}d |  |          dS )z@nous:idr"  r6   )r   midr  s     r'   r   z!get_providers.<locals>.<listcomp>l  sC         .~~8J8J38O8OPP  r)   z1Failed to load Nous Portal models from hermes_clir\   c                    g | ]}||d S r  r6   )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  r6   r   ks     r'   r   z!get_providers.<locals>.<listcomp>  s     &X&X&Xa!'<'<&X&X&Xr)   c                    g | ]}||d S r  r6   r  s     r'   r   z!get_providers.<locals>.<listcomp>  s     &Q&Q&Qa!'<'<&Q&Q&Qr)   )	r  r  has_keyconfigurableis_oauthr  
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  r  r  r  	is_customr  r  r  r   c                    |                      d          pd}|k    rd|fS |                    d          rd|fS |                      d          rd|fS d|fS )	Nr  r   r   r   r:   r  r-      )r   r|   )pr#   r   s     r'   _provider_sort_keyz)get_providers.<locals>._provider_sort_key  sn    eeDkkR/!!s8O>>)$$ 	s8O55 	s8O3xr)   rf  )r   r   )1setr   keysr   r   r   r   r   updater  sortedr   r-  r  r   hermes_cli.authr  r@   r  r  ra   ro   r   r   r   r   rc   rematchr   r   r   r   r   r   r   hermes_cli.modelsr
  rB  r  r  r   r    r   warningr   r|   r   r  )+r   	known_idsr   r   r#   r  r  r  r  r  _gasr  r   rp   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  r   s+                                            @@r'   get_providersr@    s'
    I %*,,--4D4I4K4K0L0LLI ,,CGGK,,M-&& /++--... %&&&i   B B(,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
4S*..:Q:QRR 3!+JJ6sBIg<N<NOO 3!*JJ $G!:!>!>sB!G!G!M2 " "<S*..QVBWBWXX ")3J&*G!E<S")EBRBRSS ")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 (??7?/WX[/\/\?&*333446{{ .  4^DDH:<< ) )h&&OOC(((8hGGK +$"6{{+8-k:: K  +$"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# #I	 	 	 	 	 	 O$$I)T"" 4#--
33     NN)N*** *  sK   4B3G))'HHAN  
N-,N-AS""$T	T	0U$U+*U+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.)r  r  rx   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: Tr  removed)r  r   r  action)r    r!   r  r   r   ra   r   ro   r   r   r   r@   r  	exceptionr   )r   r   r   rp   r  s        r'   set_provider_keyrF    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.
    Nr  )rF  r   _clean_provider_key_from_config)r   r6  s     r'   remove_provider_keyrI    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_lockNFrr   rs   r   r   Tr   r   r   r   z4Failed to clean provider key from config.yaml for %s)rB  rK  r	  _get_config_pathr@   ry   yamlrz   	safe_loadr   r   r   r   r    r!   r   r(   r   r   r  rE  )r   rK  _configconfig_path_yamlchangedr   r   r   r   r   r   r   r   s                 r'   rH  rH  0  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   r0   )r   r8   )r   r	   )rp   r	   r   r`   )r   r   r   r   )r   r   r   r   )r   r   r   r   r   r   )r   r   r   r   )rp   r	   r   r   r   r8   )r   r   r   r   )r   r   )r   r   r   r  )r   r   r   r  )r   r   r   r   )r  r   r   r  )r   r   r9  r   r   r   r   r   )rn   r	   r   r   r   r   r   r`   )r   r   r   r   )r   r   rn   r	   r   r   r   rL  )rT  rL  r   rU  r2   )r   r   r   r8   )rT  rL  r  r   r   r8   )r   r   rn   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   r  )r   r   r  r  r   r8   )r   r   r  r  r  r  r   r  )r  r  r  r	  r   r  )Nr  )r   r   r  r	  r   r   )r   r   )r   r   r   r   r   r   )r   r   r   r   )r   r   r   r8   )k__doc__
__future__r   r   rO  r   loggingr   r>   r|  rz  r3   rW  urllib.errorr  urllib.request
contextlibr   r   r   r   r   pathlibr	   typesr
   typingr   r  rm   rB  r   r   r   r   r   r   r   r   r   r   r   	getLogger__name__r  r(   r  r  r  rZ  rh  	frozensetr  r5   r  r.   __annotations__r/   LockrY  r7   rB   r  ra   rc   r  ro   r   r   r   r   r   r   r   r  r  r  r  r  r7  r=  rG  rK  rS  r]  ra  rk  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r   r@  rF  rI  rH  r6   r)   r'   <module>ra     s     # " " " " "     				      



              2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2       ! ! ! ! ! !      LLLL   EEE                          
	8	$	$
 
 
 
 9 "% ,0 )#'  #%  $9nk%BCC  () $. )$ EI  H H H H NP  O O O O#19>#3#3  * * * *    b" R$%&$%$$% $% 	$%
 $% 
=$% >$% "$%  $% &$% "$% M$% $% *$% ($%0 $1$%D E$%F G$% $%  $ $ $ $^ %9      9      ' ' ' '   "6 6 6 6$b b b b   )T )T )T )TX_ _ _ _D6 6 6 6r0 0 0 0f      	 	 	 	   ) ) ) )X IMdh M M M M M M   >   6P P P P
 
 
 
; ; ; ; ;> > > >6 ]a .7 .7 .7 .7 .7 .7b QV      D Y^ ! ! ! ! ! !H^
% ^
 ^
 ^
 ^
 ^
 ^
B+ + + + ,  $in&& 9 9 9 9       B" " " "J   2   D   BR R R R RpY Y Y Yx7 7 7 7t   *I^ I^ I^ I^ I^ I^s   A$ $A.-A.