+
    iHàjÄ  ã                  óŠ   € R t ^ RIHt ^ RIt^ RIHtHt RtR R ltR R lt	RR	 R
 llt
RR R lltRR R lltRR R lltR# )u5  Conversation-affinity request headers for session-aware relays and proxies.

Two sources, one merge point:

* ``x-opencode-session`` â€” OpenCode (opencode.ai Zen/Go relay) pins requests that share this
  value to the same upstream backend, which keeps its prompt cache warm across the turns of one
  conversation. Always sent to OpenCode targets.
* ``providers.<name>.session_affinity_header`` â€” an opt-in header NAME on a custom provider entry
  (default off). Session-aware proxies fronting a stateful backend (LiteLLM's ``x-litellm-session-id``,
  self-hosted Claude/OpenAI gateways) otherwise classify an agent-loop request whose last message is
  a ``tool_result`` as a new conversation and replay the whole history upstream (#86241, #104449).

The value only has to be opaque and consistent per conversation, so it is derived the same way as
the other affinity hints Hermes already sends (OpenRouter's sticky ``session_id``, xAI's
``x-grok-conv-id``): the host-declared routing scope first (a host that names its own conversation,
#96811), then the ambient conversation ROOT (stable across compaction rotation and delegate trees),
then the physical session id â€” normalized through ``_cache_scope_from_session_id`` so cron fires of
one job share a scope. Auxiliary calls (compression, titles, vision, MoA) have no session handle and
resolve the ambient value, so they stay on the conversation's backend too (#70820).

Every request â€” main turn on any transport, auxiliary calls â€” goes through
:func:`merge_session_affinity_headers` so the headers cannot drift per code path.
)ÚannotationsN)ÚAnyÚOptionalzx-opencode-sessionc               ó(   € V ^8„  d   QhRRRRRRRR/# )é   ÚproviderúOptional[str]ÚmodelÚbase_urlÚreturnztuple[Optional[str], str]© )Úformats   "Ú</home/ubuntu/.hermes/hermes-agent/agent/opencode_affinity.pyÚ__annotate__r   !   s0   € ÷ Jñ J ð J°}ð JÐP]ð JÐb{ñ Jó    c                óD  € ^ RI HpHpHp ^ RIHpHp \        T;'       g    R4      pT! \        T ;'       g    R4      V4      p	V	f   RV3# T! \        T ;'       g    R4      4      ;'       g    / P                  R4      '       d   RV3# V! W”! W4      4      p
W£! W
V4      3# )aÃ  ``(api_mode, base_url)`` re-derived per model for an OpenCode relay target; ``(None, base_url)`` otherwise.

OpenCode Zen/Go serve Responses-only (``gpt-*``, ``grok-*``, ``muse-spark``), Anthropic-wire
(``minimax-*``, ``qwen*``, ``claude-*``) and chat/completions models behind one provider, so a
provider-level or persisted ``api_mode`` is wrong for every model but the one it was saved for.
The main runtime (``hermes_cli/runtime_provider.py``) always re-derives from the effective model;
auxiliary resolution must agree or ``gpt-5.6-luna`` compression 500s on /chat/completions (#98799).
Built-in families, custom entries named after one (``opencode-go-bridge``, #85589) and opencode.ai
hosts all count.
)Únormalize_opencode_base_urlÚnormalize_opencode_model_idÚopencode_model_api_mode)Ú_get_named_custom_providerÚ_opencode_family_for_customÚ NÚapi_mode)	Úhermes_cli.modelsr   r   r   Ú"hermes_cli.runtime_provider_customr   r   ÚstrÚget)r   r	   r
   r   r   r   r   r   ÚurlÚfamilyr   s   &&&        r   Úopencode_transportr   !   sž   € ÷ tÑsßjä
ˆh�nˆn˜"Ó
€CÙ(¬¨X¯^¨^¸Ó)<¸cÓB€FØ‚~Ø�SˆyÐñ 	#¤3 x§~ ~°2Ó#6Ó7×=Ð=¸2×BÑBÀ:×NÒNØ�SˆyÐá& vÐ/JÈ8Ó/[Ó\€HØÐ0°ÀSÓIÐIÐIr   c               ó$   € V ^8„  d   QhRRRRRR/# )r   r   r   r
   r   Úboolr   )r   s   "r   r   r   <   s!   € ÷ ñ  ð ¸-ð ÈDñ r   c                ó´   €  ^ RI Hp V! V 4      e   R#   ^ RIHp T! \        T;'       g    R4      4      #   \         d     L/i ; i  \         d     R# i ; i)zÄTrue when *provider* or *base_url* addresses the OpenCode relay.

Matches the built-in opencode-zen/go providers, custom
``opencode-<family>-*`` providers, and any base_url hosted on opencode.ai.
)Úopencode_provider_familyT)Ú_is_opencode_endpointr   F)r   r#   Ú	ExceptionÚagent.anthropic_endpointsr$   r   )r   r
   r#   r$   s   &&  r   Úis_opencode_targetr'   <   sb   € ðÝ>á# HÓ-Ò9Ùð :ðÝCá$¤S¨¯¨°RÓ%8Ó9Ð9øô ô Ùðûô ô Úðús%   ‚7 –A ¬
A ·AÁAÁAÁAc               ó    € V ^8„  d   QhRRRR/# )r   Ú
session_idr   r   r   r   )r   s   "r   r   r   Q   s   € ÷ %ñ % ]ð %¸cñ %r   c                ó¸   €  ^ RI HpHp ^ RIHp T! V! 4       ;'       g    V! 4       ;'       g    T 4      #   \
         d    \        T ;'       g    R4      u # i ; i)zRReturn the normalized rotation-stable conversation affinity key ("" when unknown).)Úget_affinity_scopeÚget_conversation_context)Ú_cache_scope_from_session_idr   )Úagent.portal_tagsr+   r,   Úagent.transports.codexr-   r%   r   )r)   r+   r,   r-   s   &   r   Úresolve_affinity_keyr0   Q   sS   € ð%ßRÝGá+Ñ,>Ó,@×,lÐ,lÑD\ÓD^×,lÐ,lÐblÓmÐmøÜô %Ü�:×#Ð# Ó$Ò$ð%ús   ‚6  6 ¯6 ¶AÁAÁAc               ó(   € V ^8„  d   QhRRRRRRRR/# )r   r   r   r
   r)   r   údict[str, str]r   )r   s   "r   r   r   \   s0   € ÷ *ñ *Øð*àð*ð ð*ð ñ	*r   c                ó°   € \        W4      '       g   / # \        V4      pV'       g*   R\        P                  ! 4       P                  R,           2p\
        V/# )a  Return ``{"x-opencode-session": <key>}`` for OpenCode targets, else ``{}``.

OpenCode targets always get a key: when no conversation/session key resolves, an
ephemeral ``oneshot-<hex>`` value is generated (OpenCode Go rejects requests without
the header, #105841).zoneshot-:Né   N)r'   r0   ÚuuidÚuuid4ÚhexÚOPENCODE_SESSION_HEADER)r   r
   r)   Úkeys   &&& r   Úopencode_session_headersr:   \   sL   € ô ˜h×1Ò1Øˆ	Ü
˜zÓ
*€Cßð
 œŸš›×)Ñ)¨#Õ.Ð/Ð0ˆÜ# SÐ)Ð)r   c               ó$   € V ^8„  d   QhRRRRRR/# )r   r
   r   r)   r   r2   r   )r   s   "r   r   r   r   s&   € ÷ (ñ (Øð(àð(ð ñ(r   c                ó°   €  ^ RI Hp T! \        T ;'       g    R4      4      pT'       g   / # \	        T4      pT'       d   Y4/# / #   \         d    / u # i ; i)zhReturn ``{<session_affinity_header>: <key>}`` when the route's provider entry declares one, else ``{}``.)Ú+get_custom_provider_session_affinity_headerr   )Úhermes_cli.configr=   r   r%   r0   )r
   r)   r=   Úheaderr9   s   &&   r   Ú(custom_provider_session_affinity_headersr@   r   sX   € ð
ÝQá<¼SÀÇÀÈRÓ=PÓQˆ÷ Øˆ	Ü
˜zÓ
*€CßˆFˆ=Ð' RÐ'øô ô ØŠ	ðús   ‚A ˜A ÁAÁAc          
     ó,   € V ^8„  d   QhRRRRRRRRRR/# )r   Úkwargszdict[str, Any]r   r   r
   r)   r   r   )r   s   "r   r   r   ƒ   s:   € ÷ ñ Øðàðð ðð ð	ð
 ñr   c                ó&  € \        WV4      pVP                  \        W#4      4       V'       dc   V P                  R4      p\	        V\
        4      '       d   \        V4      M/ pVP                  4        F  w  rxVP                  Wx4       K  	  W`R&   V # )zÏMerge the affinity header(s) into ``kwargs["extra_headers"]`` (in place).

Existing per-request headers win, so a caller-pinned value is preserved.
Targets with neither source configured are left untouched.
Úextra_headers)r:   Úupdater@   r   Ú
isinstanceÚdictÚitemsÚ
setdefault)	rB   r   r
   r)   ÚheadersÚexistingÚmergedr9   Úvalues	   &&&&     r   Úmerge_session_affinity_headersrN   ƒ   sw   € ô ' x¸:ÓF€GØ‡N�NÔ;¸HÓQÔRßØ—:‘:˜oÓ.ˆÜ#-¨h¼×#=Ò#=”�h”À2ˆØ!Ÿ-™-ž/‰JˆCØ×Ñ˜cÖ)ñ *à"(ˆÑØ€Mr   )N)Ú__doc__Ú
__future__r   r5   Útypingr   r   r8   r   r'   r0   r:   r@   rN   r   r   r   Ú<module>rR      s?   ðñõ0 #ã ß  à.Ð õJõ6÷*%÷*÷,(÷"ñ r   