
    `gjgA                       d Z ddlm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mZ ddlmZ dd	lmZ  ej.                  e      Zd
ZdZe G d d             Z	 d	 	 	 ddZ	 	 	 	 	 	 ddZ	 ddd	 	 	 	 	 	 	 ddZddZ d dZ!d!dZ"dd	 	 	 	 	 d"dZ#dd	 	 	 	 	 d#dZ$	 d$dd	 	 	 	 	 d%dZ%	 d$dd	 	 	 	 	 	 	 d&dZ&g dZ'y)'u  Azure Foundry endpoint auto-detection.

Inspect a Microsoft Foundry / Azure OpenAI endpoint to determine:
  - API transport (OpenAI-style ``chat_completions`` vs
    Anthropic-style ``anthropic_messages``)
  - Available models (best effort — Azure does not expose a deployment
    listing via the inference API key, but Azure OpenAI v1 endpoints
    return the resource's model catalog via ``GET /models``)
  - Context length for each discovered/entered model, via the existing
    :func:`agent.model_metadata.get_model_context_length` resolver.

Rationale:

Azure has no pure-API-key deployment-listing endpoint — per Microsoft,
deployment enumeration requires ARM management-plane auth.  Azure
OpenAI v1 endpoints ``{resource}.openai.azure.com/openai/v1`` do return
a ``/models`` list, but it reflects the resource's *available* models
rather than the user's *deployed* deployment names.  In practice it is
still a useful hint — the user picks a familiar model name and we look
up its context length from the catalog.

Authentication modes:
  - ``api_key`` (default): the wizard passes an ``api_key`` string; the
    probe sends both ``api-key:`` and ``Authorization: Bearer`` headers
    so we hit any Azure deployment regardless of which header it expects.
  - ``entra_id``: the wizard passes a ``token_provider`` callable from
    :mod:`agent.azure_identity_adapter`. The probe mints exactly one
    bearer JWT, sends **only** ``Authorization: Bearer <jwt>`` (never
    ``api-key:``), and never persists the token. This matches Microsoft's
    documented contract for keyless inference.

The detector never crashes on errors (every HTTP call is wrapped in a
broad try/except).  Callers get a :class:`DetectionResult` with whatever
information could be gathered, and fall back to manual entry for the
rest.
    )annotationsN)	dataclassfield)AnyCallableOptional)request)	HTTPErrorURLError)urlparse)open_credentialed_url)z2025-04-01-previewz
2024-10-21z
2025-04-15c                  t    e Zd ZU dZdZded<    ee      Zded<   dZ	d	ed
<   dZ
d	ed<   dZded<   dZded<   y)DetectionResultzAEverything auto-detection could gather from a base URL + API key.NOptional[str]api_mode)default_factory	list[str]models strhostnamereasonFboolmodels_probe_okis_anthropic)__name__
__module____qualname____doc__r   __annotations__r   listr   r   r   r   r        J/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/azure_detect.pyr   r   C   sR    K #Hm" d3FI3 Hc FC "OT! L$r#   r   c                   |&t        |      r	  |       }|rt        |      dfS ddfS t        |       r+t        | t              s	  |        }|rt        |      dfS ddfS t        | t              r| r| dfS y# t        $ r }t        j	                  d|       Y d}~yd}~ww xY w# t        $ r }t        j	                  d|       Y d}~yd}~ww xY w)u  Coerce wizard inputs into a (token, mode) pair.

    Returns ``(token_or_None, mode)`` where ``mode`` is:
      - ``"entra_id"`` when a callable token provider was supplied — the
        returned token is a freshly minted bearer JWT, sent ONLY in
        ``Authorization: Bearer``.
      - ``"api_key"`` when a string key was supplied — the returned token
        is the raw API key, sent in BOTH ``api-key:`` and
        ``Authorization: Bearer`` headers (preserves the original
        broad-compat probe behaviour).
      - ``("", "api_key")`` when neither yields a value.

    Bearer minting failures degrade to ``("", "entra_id")`` so the caller
    can still report "detection incomplete" rather than crashing.
    Nentra_idz'azure_detect: token_provider failed: %s)Nr&   z)azure_detect: api_key callable failed: %sapi_key)Nr'   )callabler   	Exceptionloggerdebug
isinstance)r'   token_providertokenexcs       r$   _resolve_credentialr0   ^   s    & !h~&>	$"$E"'CJJ>>TJ>> GS!9	$IE"'CJJ>>TJ>>
 '3G	!!  	$LLBCH#	$  	$LLDcJ#	$s:   A6 A6 B" B" 6	B?BB"	C+CCc                    |sy|dk(  r| j                  dd|        y| j                  d|       | j                  dd|        y)zBAttach the right auth headers to ``req`` based on credential mode.Nr&   AuthorizationzBearer zapi-key)
add_header)reqr.   modes      r$   _apply_auth_headersr6      sN     z 	'%(9: 	y%('%(9:r#   r-   c                  t        ||      \  }}t        j                  | d      }t        |||       |j	                  dd       	 t        ||      5 }|j                         }	 |j                  t        j                  |j                  dd            fcd	d	d	       S # t        $ r |j                  d	fcY cd	d	d	       S w xY w# 1 sw Y   y	xY w# t        $ r}	|	j                  d	fcY d	}	~	S d	}	~	wt        t        t         f$ r!}	t"        j%                  d
| |	       Y d	}	~	yd	}	~	wt        $ r!}	t"        j%                  d| |	       Y d	}	~	yd	}	~	ww xY w)zoGET a URL with the appropriate auth headers.  Return
    ``(status_code, parsed_json_or_None)``.  Never raises.GET)method
User-Agenthermes-agent/azure-detecttimeoututf-8replaceerrorsNzazure_detect: GET %s failed: %s)r   Nz)azure_detect: GET %s unexpected error: %s)r0   urllib_requestRequestr6   r3   r   readstatusjsonloadsdecoder)   r
   coder   TimeoutErrorOSErrorr*   r+   )
urlr'   r>   r-   r.   r5   r4   respbodyr/   s
             r$   _http_get_jsonrP      s(    &g~>KE4

 
 U
3CUD)NN<!<="38 	)D99;D){{DJJt{{79{/U$VV	) 	)  ){{D((	) 	))		) 	)  xx~lG, 6SA @#sKsx   C C&1B!	C !C8C9	C CCCC C 	EC/)E/ED""E.E

Ec                N    t        j                  dd| j                  d            S )zAStrip trailing ``/v1`` or ``/v1/`` so we can construct sub-paths.z/v1/?$r   /)resubrstrip)rM   s    r$   _strip_trailing_v1rV      s    66)RC11r#   c                    	 t        |       }|j                  xs dj                         j                  d      }|j	                  d      xs d|dz   v S # t
        $ r Y yw xY w)zReturn True when the URL's path ends in ``/anthropic`` or
    contains a ``/anthropic/`` segment.  Used by Azure Foundry
    resources that route Claude traffic through a dedicated path.r   rR   z
/anthropicz/anthropic/F)r   pathlowerrU   endswithr)   )rM   parsedrX   s      r$   _looks_like_anthropic_pathr\      sa    #!r((*11#6}}\*Imtcz.II s   AA 	A A c                \   t        | t              r| j                  d      nd}t        |t              sg S g }|D ]o  }t        |t              s|j                  d      xs$ |j                  d      xs |j                  d      }t        |t              s\|s_|j                  |       q |S )zrExtract a list of model IDs from an OpenAI-shaped ``/models``
    response.  Returns ``[]`` on any shape mismatch.dataNidmodelname)r,   dictgetr!   r   append)payloadr^   idsitemmids        r$   _extract_model_idsri      s     #-Wd";7;;vDdD!	C $%hhtnE 1ETXXf5Ec3CJJsO Jr#   c               X   | j                  d      } |  dg}t        D ]  }|j                  |  d|         |D ]j  }t        |||      \  }}|dk(  s|t	        |      }|r&t
        j                  d|t        |             d|fc S t        |t              sad|v sfdg fc S  d	g fS )
zProbe ``<base>/models`` for an OpenAI-shaped response.

    Returns ``(ok, models)``.  ``ok`` is True iff the endpoint accepted
    us as an OpenAI-style caller (200 OK + OpenAI-shaped JSON body).
    rR   z/modelsz/models?api-version=r7      z0azure_detect: /models probe OK at %s (%d models)Tr^   F)
rU    _AZURE_OPENAI_PROBE_API_VERSIONSrd   rP   ri   r*   infolenr,   rb   )	base_urlr'   r-   
candidatesvrM   rF   rO   rf   s	            r$   _probe_openai_modelsrr      s     s#H Jg&'J- @XJ&:1#>?@   %c7>RS=T-$T*CFS Sy  $%&D.Rx  "9r#   c                  t        |       }| dt         }t        j                  dddddgd      j	                  d      }t        j                  |d	|
      }t        ||      \  }}t        |||       |j                  dd       |j                  dd       |j                  dd       	 t        |d      5 }	|	j                  dk  cddd       S # 1 sw Y   yxY w# t        $ r|}
	 |
j                         j                  dd      }|j                         }d|v sd|v r	d|v rY d}
~
y|
j                   dk(  rd|v sd|v rY d}
~
yY d}
~
y# t"        $ r Y Y d}
~
yw xY wd}
~
wt$        t&        t(        f$ r Y yt"        $ r Y yw xY w)a'  Send a zero-token request to ``<base>/v1/messages`` and check
    whether the endpoint at least *recognises* the Anthropic Messages
    shape (any 4xx that mentions ``messages`` or ``model``, or a 400
    ``invalid_request`` with an Anthropic error shape).  Never completes
    a real chat.
    z/v1/messages?api-version=probe   userping)rolecontent)r`   
max_tokensmessagesr?   POST)r:   r^   zanthropic-versionz
2023-06-01zcontent-typezapplication/jsonr;   r<         @r=   i  Nr@   rA   	anthropicz"type"z"error"Ti  r{   r`   F)rV   _AZURE_ANTHROPIC_API_VERSIONrG   dumpsencoderC   rD   r0   r6   r3   r   rF   r
   rE   rI   rY   rJ   r)   r   rK   rL   )ro   r'   r-   baserM   re   r4   r.   r5   rN   r/   rO   lowereds                r$   _probe_anthropic_messagesr      s    h'DF+,H+I
JCjj$89  vg	 
 
 
 V'
BC%g~>KE4UD)NN&5NN>#56NN<!<="34 	% ;;$	% 	% 	%  	88:$$WY$?DjjlGg%W)<gAU xx3J'$9W=O 		lG,  s`   .C  ;C
	C  CC  C   	F*=E,E	EE EE  F9FFc                  t               }	 t        |       }|j                  xs dj                         |_        t        |       rd|_        d|_        d|_        |S t        | ||      \  }}|r6d|_
        ||_        d|_        |rdt        |       d|_        |S d	|_        |S t        | ||      rd|_        d|_        d
|_        |S d|_        |S # t        $ r
 d|_        Y w xY w)u}  Inspect an Azure endpoint and describe its transport + models.

    Call this from the wizard before asking the user to pick an API
    mode manually.  The caller should treat the returned
    :class:`DetectionResult` as *advisory* — if ``api_mode`` is None,
    fall back to asking the user.

    ``api_key`` may be a string (legacy API-key auth — sends both
    ``api-key:`` and ``Authorization: Bearer``) or a callable returning
    a bearer JWT (Entra ID auth — sends ONLY ``Authorization: Bearer``).
    ``token_provider`` is an alternative explicit name for the callable
    form; if both are supplied the callable wins.
    r   Tanthropic_messagesu6   URL path ends in /anthropic → Anthropic Messages APIr7   chat_completionszGET /models returned u#    model(s) — OpenAI-style endpointuJ   GET /models returned an OpenAI-shaped empty list — OpenAI-style endpointz)Endpoint accepts Anthropic Messages shapeu   Could not probe endpoint (private network, missing model list, or non-standard path) — falling back to manual API-mode selection)r   r   r   rY   r)   r\   r   r   r   rr   r   r   rn   r   )ro   r'   r-   resultr[   okr   s          r$   detectr   +  s   $ F(#!??0b779 "(+".P &hWJB	!%,  $CK=0ST 	
  ^ 	
 
 !7>R".C	K M MO  s   .C C&%C&c               F   t        | xs d      j                         }|sy	 ddlm}m} t        ||      \  }}|xs d}		  ||||	      }
t        |
t              r|
dkD  r|
|k7  r|
S y# t
        $ r Y yw xY w# t
        $ r }t        j                  d|       Y d}~yd}~ww xY w)uC  Thin wrapper around :func:`agent.model_metadata.get_model_context_length`
    that returns ``None`` when only the fallback default (128k) would
    fire, so the wizard can distinguish "we actually know this" from
    "we guessed.

    For Entra-ID mode pass a callable as ``api_key`` (or via
    ``token_provider=``); the wrapped resolver expects a string, so we
    mint one bearer JWT here for the single lookup. The resolver itself
    only reads catalog metadata over HTTP — no SDK client is built — so
    the minted token is consumed for at most one /models probe.
    r   Nr   )DEFAULT_FALLBACK_CONTEXTget_model_context_length)ro   r'   z.azure_detect: context length lookup failed: %s)r   stripagent.model_metadatar   r   r)   r0   r*   r+   r,   int)r`   ro   r'   r-   model_idr   r   r.   r5   effective_keynr/   s               r$   lookup_context_lengthr   l  s    " 5;B%%'H	
 &g~>KE4KRM$X-X
 !Sa!e-E(E!    EsKs)   A(  A7 (	A43A47	B  BB )r   r   r   )N)r'   r   r-   Optional[Callable[[], str]]returnztuple[Optional[str], str])r4   zurllib_request.Requestr.   r   r5   r   r   None)r}   )
rM   r   r'   r   r>   floatr-   r   r   ztuple[int, Optional[dict]])rM   r   r   r   )rM   r   r   r   )re   rb   r   r   )ro   r   r'   r   r-   r   r   ztuple[bool, list[str]])ro   r   r'   r   r-   r   r   r   )r   )ro   r   r'   r   r-   r   r   r   )
r`   r   ro   r   r'   r   r-   r   r   zOptional[int])(r   
__future__r   rG   loggingrS   dataclassesr   r   typingr   r   r   urllibr	   rC   urllib.errorr
   r   urllib.parser   hermes_cli.urllib_securityr   	getLoggerr   r*   rl   r   r   r0   r6   rP   rV   r\   ri   rr   r   r   r   __all__r"   r#   r$   <module>r      s  #J #   	 ( * * , , ! <			8	$$    ,    6 GK$(C$6$N;,;!;&*;& %( BF	! $?	
 3:2
	( HL!"%! *E! 5	!N MQ/'*/ /J/ $(	/f > :>>> 7>  	>F *,) IM	)$')#&) +F	)
  -)X Ar#   