
    Fjo                         d Z ddlZddl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 ddlmZmZmZ d	ed
edz  dedz  fdZdedz  dedz  fdZdedefdZ G d de          ZddlmZ  ede           dS )u  OpenAI Chat Completions transport.

Handles the default api_mode ('chat_completions') used by ~16 OpenAI-compatible
providers (OpenRouter, Nous, NVIDIA, Qwen, Ollama, DeepSeek, xAI, Kimi, etc.).

Messages and tools are already in OpenAI format — convert_messages and
convert_tools are near-identity.  The complexity lives in build_kwargs
which has provider-specific conditionals for max_tokens defaults,
reasoning configuration, temperature handling, and extra_body assembly.
    N)AnyDict)resolve_lmstudio_effort)is_moonshot_modelsanitize_moonshot_tools)DEVELOPER_ROLE_MODELS)ProviderTransport)NormalizedResponseToolCallUsagemodelreasoning_configreturnc                    |t          |t                    sdS | pd                                                                }|                    d          r|                    dd          d         }|                    d          sdS |                    d          du rd	diS t          |                    d
d          pd                                                                          }|dk    rd	diS d	di}|                    d          r|S |dvrd}|                    d          r-d|v r|dv rd|d<   n|dv rd|d<   nd|d<   nd|v r|dv rdnd|d<   |S )zLTranslate Hermes/OpenRouter-style reasoning config to Gemini thinkingConfig.N zgoogle//   geminienabledFincludeThoughtseffortmediumnoneTzgemini-2.5->   lowhighxhighr   minimal)zgemini-3z
gemini-3.1flash>   r   r   r   thinkingLevel>   r   r   r   pro)
isinstancedictstriplower
startswithsplitgetstr)r   r   normalized_modelr   thinking_configs        @/usr/local/lib/hermes-agent/agent/transports/chat_completions.py_build_gemini_thinking_configr,      s   z2BD'I'It**,,2244""9-- =+11#q99!< &&x00 tI&&%// "5))!%%h99EXFFLLNNTTVVF!5))'8$&?O
 ""=11 BBB
 ""#=>> &&&+++3800,,,39003;00&&& $5555 O,     configc                    t          | t                    r| sdS i }t          |                     d          t                    r| d         |d<   t          |                     d          t                    rI| d                                         r/| d                                                                         |d<   t          |                     d          t          t          f          rt          | d                   |d<   |pdS )zEConvert Gemini thinking config keys to the OpenAI-compat field names.Nr   include_thoughtsr   thinking_levelthinkingBudgetthinking_budget)	r!   r"   r'   boolr(   r#   r$   intfloat)r.   
translateds     r+   "_snake_case_gemini_thinking_configr8   N   s    fd## 6 t!#J&**.//66 C)/0A)B
%&&**_--s33 O8O8U8U8W8W O'-o'>'D'D'F'F'L'L'N'N
#$&**-..e== F(+F3C,D(E(E
$%r-   base_urlc                     t          | pd                                                              d                                          }|sdS d|vrdS |                    d          S )Nr   r   Fz!generativelanguage.googleapis.comz/openai)r(   r#   rstripr$   endswith)r9   
normalizeds     r+   !_is_gemini_openai_compat_base_urlr>   ]   sj    X^$$**,,33C88>>@@J u**<<uy)))r-   c                   t   e Zd ZdZedefd            Zdeeee	f                  deeee	f                  fdZ
deeee	f                  deeee	f                  fdZ	 dd	edeeee	f                  deeee	f                  dz  deee	f         fd
Zd Zde	defdZde	defdZde	deeef         dz  fdZdS )ChatCompletionsTransportzfTransport for api_mode='chat_completions'.

    The default path for OpenAI-compatible providers.
    r   c                     dS )Nchat_completions )selfs    r+   api_modez!ChatCompletionsTransport.api_model   s    !!r-   messagesc                 V   d}|D ]}t          |t                    sd|v sd|v sd|v rd} nrt          d |D                       rd} nU|                    d          }t          |t                    r*|D ]#}t          |t                    rd|v sd	|v rd} n$|r n|s|S t          j        |          }|D ]}t          |t                    s|                    dd
           |                    dd
           |                    dd
           d |D             D ]}|                    |d
           |                    d          }t          |t                    rF|D ]C}t          |t                    r,|                    dd
           |                    d	d
           D|S )uF  Messages are already in OpenAI format — strip internal fields
        that strict chat-completions providers reject with HTTP 400/422
        (or, in the case of some OpenAI-compatible gateways, 5xx):

        - Codex Responses API fields: ``codex_reasoning_items`` /
          ``codex_message_items`` on the message, ``call_id`` /
          ``response_item_id`` on ``tool_calls`` entries.
        - ``tool_name`` on tool-result messages — written by
          ``make_tool_result_message()`` for the SQLite FTS index, but not
          part of the Chat Completions schema. Strict providers (Fireworks,
          Moonshot/Kimi) reject any payload containing it with
          ``Extra inputs are not permitted, field: 'messages[N].tool_name'``.
          Permissive providers (OpenRouter, MiniMax) silently ignore the
          field, which masked the bug for months.
        - Hermes-internal scaffolding markers — any top-level message key
          starting with ``_`` (e.g. ``_empty_recovery_synthetic``,
          ``_empty_terminal_sentinel``, ``_thinking_prefill``). These are
          bookkeeping flags the agent loop attaches to messages so the
          persistence layer can later strip its own scaffolding; they must
          never reach the wire. Permissive providers (real OpenAI,
          Anthropic) silently drop unknown message keys, but strict
          gateways (e.g. opencode-go, codex.nekos.me) reject with
          ``Extra inputs are not permitted, field: 'messages[N]._empty_recovery_synthetic'``,
          which then poisons every subsequent request in the session.
        Fcodex_reasoning_itemscodex_message_items	tool_nameTc              3   j   K   | ].}t          |t                    o|                    d           V  /dS )_Nr!   r(   r%   .0ks     r+   	<genexpr>z<ChatCompletionsTransport.convert_messages.<locals>.<genexpr>   s=      II:a%%;!,,s*;*;IIIIIIr-   
tool_callscall_idresponse_item_idNc                 f    g | ].}t          |t                    |                    d           ,|/S )rL   rM   rN   s     r+   
<listcomp>z=ChatCompletionsTransport.convert_messages.<locals>.<listcomp>   s8    SSSa*Q*<*<ScARARSSSSr-   )r!   r"   anyr'   listcopydeepcopypop)	rD   rF   kwargsneeds_sanitizemsgrR   tc	sanitizedkeys	            r+   convert_messagesz)ChatCompletionsTransport.convert_messagesp   s   8  	 	Cc4(( '3..(C//#%%!%IISIIIII !%..J*d++ $  B!"d++ !R+=+C+C)-! E 	OM(++	 	9 	9Cc4(( GG+T222GG)4000GGK&&& TS3SSS # #T""""..J*d++ 9$ 9 9B!"d++ 9y$///14888r-   toolsc                     |S )u0   Tools are already in OpenAI format — identity.rC   )rD   rc   s     r+   convert_toolsz&ChatCompletionsTransport.convert_tools   s    r-   Nr   c                   ( |                      |          }|                    d          }|r|                     |||||          S |                    d|pd                                          (|rzt	          |d         t
                    r_|d                             d          dk    r@t          (fdt          D                       r t          |          }i |d         ddi|d<   ||d	}|                    d
          }|||d
<   |r#t          |          rt          |          }||d<   |                    d          }	|                    d          }
|                    d          }|                    d          }|                    dd          }|                    dd          }|                    dd          }|                    d          }|
!|	r|                     |	|
                     n*|!|	r|                     |	|                     n|||d<   |rt          |o+t	          |t
                    o|                    d          du           }|s_d}|rVt	          |t
                    rA|                    d          pd                                                                }|dv r|}||d<   |rt          |o+t	          |t
                    o|                    d          du           }|s_d}|rVt	          |t
                    rA|                    d          pd                                                                }|dv r|}||d<   |                    dd          r@|                    dd          r*t          ||                    d                    }|||d<   i }|                    dd          }|                    d d          }|                    d!d          }t          |                    d"          pd                                                                          }|                    d#          }|                    d$          }|r|r||d%<   |rh|d&k    rb|                    d'          }|K|dk    rE	 t!          |          }n# t"          t$          f$ r d}Y nw xY w|d(|cxk    rd)k    rn n	d*|d+g|d,<   |r=d-} |r.t	          |t
                    r|                    d          du rd} d.| rdnd/i|d0<   |                    dd          r=|                    dd          s'|r|                    d1          }!|!|!|d2<   nd-dd3|d2<   |d4k    rtt'          ||          }"t)          |          rMt+          |"          }#|#r;|                    d5i           }$|$                    d6i           }%|#|%d7<   |%|$d6<   |$|d5<   n%|"r|"|d7<   n|d8k    rt'          ||          }#|#r|#|d7<   |                    d9          }&|&r|                    |&           |r||d5<   |                    d:          }'|'r|                    |'           |S );uX  Build chat.completions.create() kwargs.

        params (all optional):
            timeout: float — API call timeout
            max_tokens: int | None — user-configured max tokens
            ephemeral_max_output_tokens: int | None — one-shot override
            max_tokens_param_fn: callable — returns {max_tokens: N} or {max_completion_tokens: N}
            reasoning_config: dict | None
            request_overrides: dict | None
            session_id: str | None
            model_lower: str — lowercase model name for pattern matching
            # Provider profile path (all per-provider quirks live in providers/)
            provider_profile: ProviderProfile | None — when present, delegates to
                _build_kwargs_from_profile(); all flag params below are bypassed.
            # Legacy-path flags — only used when provider_profile is None
            # (i.e. custom / unregistered providers). Known providers all go
            # through provider_profile.
            is_openrouter: bool
            is_nous: bool
            is_qwen_portal: bool
            is_github_models: bool
            is_nvidia_nim: bool
            is_kimi: bool
            is_tokenhub: bool
            is_lmstudio: bool
            is_custom_provider: bool
            ollama_num_ctx: int | None
            # Provider routing
            provider_preferences: dict | None
            # Qwen-specific
            qwen_prepare_fn: callable | None — runs AFTER codex sanitization
            qwen_prepare_inplace_fn: callable | None — in-place variant for deepcopied lists
            qwen_session_metadata: dict | None
            # Temperature
            fixed_temperature: Any — from _fixed_temperature_for_model()
            omit_temperature: bool
            # Reasoning
            supports_reasoning: bool
            github_reasoning_extra: dict | None
            lmstudio_reasoning_options: list[str] | None  # raw allowed_options from /api/v1/models
            # Claude on OpenRouter/Nous max output
            anthropic_max_output: int | None
            extra_body_additions: dict | None
        provider_profilemodel_lowerr   r   rolesystemc              3       K   | ]}|v V  	d S NrC   )rO   prh   s     r+   rQ   z8ChatCompletionsTransport.build_kwargs.<locals>.<genexpr>  s(      DDA$DDDDDDr-   	developerr   rF   timeoutNrc   max_tokens_param_fnephemeral_max_output_tokens
max_tokensanthropic_max_outputis_nvidia_nimFis_kimiis_tokenhubr   r   r   r   >   r   r   r   reasoning_effortr   is_lmstudiosupports_reasoninglmstudio_reasoning_optionsis_openrouteris_nousis_github_modelsprovider_namer9   provider_preferencesproviderzopenrouter/pareto-codeopenrouter_min_coding_scoreg        g      ?zpareto-router)idmin_coding_scorepluginsTtypedisabledthinkinggithub_reasoning_extra	reasoning)r   r   r   
extra_bodygoogler*   zgoogle-gemini-cliextra_body_additionsrequest_overrides)rb   r'   _build_kwargs_from_profiler$   r!   r"   rW   r   rX   r   r   updater4   r#   r   r(   r6   	TypeError
ValueErrorr,   r>   r8   ))rD   r   rF   rc   paramsr`   _profile
api_kwargsrp   max_tokens_fn	ephemeralrs   anthropic_max_outru   rv   rw   r   _kimi_thinking_off_kimi_effort_e_tokenhub_thinking_off_tokenhub_effort
_lm_effortr   r|   r}   r~   r   r9   provider_prefs_pareto_score_pareto_score_f_kimi_thinking_enabledgh_reasoningraw_thinking_configr*   openai_compat_extragoogle_extra	additions	overridesrh   s)                                           @r+   build_kwargsz%ChatCompletionsTransport.build_kwargs   s   h ))(33	 ::011 	22%E6   jj"0C0C0E0EFF	A9Q<..	A !  ((H44DDDD.CDDDDD 5 YI@il@FK@@IaL !&
 &


 **Y''$+Jy!  	( !'' 7/66"'Jw 

#899JJ<==	ZZ--
"JJ'=>>

?E::**Y..jj66!::&899 ] mmI667777##mmJ778888*'8J|$  	>!%  =/66=$((33u<" "
 & >'# *
3CT(J(J **..x88>BEEGGMMOOB666')1=
-.  	B%)  =/66=$((33u<& &"
 * B#) # .
3CT(J(J .*..x88>BEEGGMMOOB666+-(1A
-. ::mU++ 	<

;OQV0W0W 	<0 

788 J %1;
-. &(


?E::**Y..!::&8%@@FJJ77=2>>DDFFLLNN::j))$:;; 	4m 	4%3Jz"
  
	U&>>>"JJ'DEEM(]b-@-@+&+M&:&:OO!:. + + +&*OOO+".3/3P3P3P3PS3P3P3P3P3P.OTT-Jy)
  	%)" 3J/?$F$F 3#''	22e;;-2*%;K		&Jz" ::*E22 	P6::mUZ;[;[ 	P P%zz*BCC+.:J{+6:h*O*O
;'H$$"?GW"X"X0:: 	D"DEX"Y"Y" C*4..r*J*J'#6#:#:8R#H#HL6EL!234@'1/BJ|,$ D0C
,-111;ECSTTO @0?
,- JJ566	 	)i((( 	2'1J|$ JJ233	 	)i(((s   "R2 2SSc           
         ddl m} |                    |          }|pd                                |rzt	          |d         t
                    r_|d                             d          dk    r@t          fdt          D                       r t          |          }i |d         ddi|d<   ||d}|j
        |u rn.|j
        |j
        |d
<   n|                    d
          }|||d
<   |                    d          }	|	|	|d<   |r#t          |          rt          |          }||d<   |                    d          }
|                    d          }|                    d          }|                    d          }|                    |          }|!|
r|                     |
|                     nM|!|
r|                     |
|                     n*|r!|
r|                     |
|                     n|||d<   |                    d          }|                    ||                    dd          |                    d          ||                    d          |                    d                    \  }}|                    |           i }|                    |                    d          |                    d          ||                    d          ||                    d                    }|r|                    |           |r|                    |           |                    d          }|r|                    |           |                    d          }|rP|                                D ];\  }}|dk    r+t	          |t
                    r|                    |           6|||<   <|r||d<   |S )u   Build API kwargs using a ProviderProfile — single path, no legacy flags.

        This method replaces the entire flag-based kwargs assembly when a
        provider_profile is passed. Every quirk comes from the profile object.
        r   )OMIT_TEMPERATUREr   ri   rj   c              3       K   | ]}|v V  	d S rl   rC   )rO   rm   _model_lowers     r+   rQ   zFChatCompletionsTransport._build_kwargs_from_profile.<locals>.<genexpr>  s(      EE!A%EEEEEEr-   rn   ro   Ntemperaturerp   rc   rq   rr   rs   rt   r   rz   Fqwen_session_metadataollama_num_ctx
session_id)r   rz   r   r   r   r   r   r9   r   )r   r   r   r9   r   r   r   r   r   )providers.baser   prepare_messagesr$   r!   r"   r'   rW   r   rX   fixed_temperaturer   r   get_max_tokensr   build_api_kwargs_extrasbuild_extra_bodyitems)rD   profiler   r`   rc   r   r   r   temprp   r   r   user_maxanthropic_maxprofile_maxr   extra_body_from_profiletop_level_from_profiler   profile_bodyr   r   rP   vr   s                           @r+   r   z3ChatCompletionsTransport._build_kwargs_from_profile  sv    	433333 ,,Y77	 **,,	A9Q<..	A !  ((H44EEEE/DEEEEE 5 YI@il@FK@@IaL !&
 &

 $(888&2(/(AJ}%% ::m,,D,0
=) **Y''$+Jy!  	( '' 7/66"'Jw 

#899JJ<==	::l++

#9:: ,,U33 ] mmI667777!m!mmH556666 	5] 	5mmK889999&'4J|$ "::&899++!1#)::.BE#J#J&,jj1H&I&I%zz*:;;!::l33 ,   	8!7 	0111 &(
 //zz,//!',B!C!CZZ
++-(.

3P(Q(Q 0 
 
  	,l+++ # 	75666 JJ566	 	)i((( JJ233	 	&!)) & &1$$At)<)<$%%a(((($%JqMM 	2'1J|$r-   responsec           	         |j         d         }|j        }|j        pd}d}|j        rg }|j        D ]}i }t	          |dd          }	|	,t          |d          r|j        pi                     d          }	|	;t          |	d          r&	 |	                                }	n# t          $ r Y nw xY w|	|d<   |
                    t          |j        |j        j        |j        j        |pd                     d}
t          |d          rS|j        rL|j        }t#          t	          |d	d          pdt	          |d
d          pdt	          |dd          pd          }
t	          |dd          }t	          |dd          }|Dt          |d          r4t	          |dd          pi }t%          |t&                    rd|v r|d         }i }|||d<   t	          |dd          }|r||d<   t)          |j        ||||
|pd          S )u  Normalize OpenAI ChatCompletion to NormalizedResponse.

        For chat_completions, this is near-identity — the response is already
        in OpenAI format.  extra_content on tool_calls (Gemini thought_signature)
        is preserved via ToolCall.provider_data.  reasoning_details (OpenRouter
        unified format) and reasoning_content (DeepSeek/Moonshot) are also
        preserved for downstream replay.
        r   stopNextra_contentmodel_extra
model_dump)r   name	argumentsprovider_datausageprompt_tokenscompletion_tokenstotal_tokens)r   r   r   r   reasoning_contentreasoning_details)contentrR   finish_reasonr   r   r   )choicesmessager   rR   getattrhasattrr   r'   r   	Exceptionappendr   r   functionr   r   r   r   r!   r"   r
   r   )rD   r   r\   choicer^   r   rR   r_   tc_provider_dataextrar   ur   r   r   r   rds                    r+   normalize_responsez+ChatCompletionsTransport.normalize_response!  s    !!$n,6
> 	Jn  
 46 OT::=WR%?%?=^1r66GGE$ul33 !!$)$4$4$6$6EE( ! ! ! D!8=$_5!!5[-"$+"7&6&>$	      8W%% 	(. 	A%a!<<A")!-@!"D"D"I$Q::?a  E Cd33	#C)<dCC$m)D)D$!#}d;;ArK+t,, E1D1S1S$/0C$D!(*(1BM-.S-t44 	413M-.!K!''/4
 
 
 	
s   B
B('B(c                 R    |dS t          |d          r|j        dS |j        sdS dS )z&Check that response has valid choices.NFr   T)r   r   )rD   r   s     r+   validate_responsez*ChatCompletionsTransport.validate_responsen  sA    5x++ 	x/?/G5 	5tr-   c                     t          |dd          }|dS t          |dd          }|dS t          |dd          pd}t          |dd          pd}|s|r||dS dS )zAExtract OpenRouter/OpenAI cache stats from prompt_tokens_details.r   Nprompt_tokens_detailscached_tokensr   cache_write_tokens)r   creation_tokens)r   )rD   r   r   detailscachedwrittens         r+   extract_cache_statsz,ChatCompletionsTransport.extract_cache_statsx  s    '400=4%!8$???4/155:'#7;;@q 	IW 	I%+HHHtr-   rl   )__name__
__module____qualname____doc__propertyr(   rE   rX   r"   r   rb   re   r   r   r
   r   r4   r   r5   r   rC   r-   r+   r@   r@   f   s        
 "# " " " X"JT#s(^,J	d38n	J J J JX4S#X#7 Dc3h<P     .2	g gg tCH~&g DcN#d*	g 
c3hg g g gRv v vpK
3 K
=O K
 K
 K
 K
Z# $    C DcNT4I      r-   r@   )register_transportrB   )r   rY   typingr   r   agent.lmstudio_reasoningr   agent.moonshot_schemar   r   agent.prompt_builderr   agent.transports.baser	   agent.transports.typesr
   r   r   r(   r"   r,   r8   r4   r>   r@   agent.transportsr   rC   r-   r+   <module>r      s  	 	          < < < < < < L L L L L L L L 6 6 6 6 6 6 3 3 3 3 3 3 F F F F F F F F F F5 5t 5PTW[P[ 5 5 5 5ptd{ td{    * * * * * *^ ^ ^ ^ ^0 ^ ^ ^D 0 / / / / /  %'? @ @ @ @ @r-   