
    JjP                       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mZ ddl	m
Z
mZmZmZ ddlmZmZmZmZmZmZmZ  ej        e          ZdZdZeefZd	d
ddZdZdZd+dZd,dZ d-dZ!d.dZ"d/d"Z# G d# d$e          Z$d0d&Z%d1d*Z&dS )2u  OpenRouter-compatible image generation backend (OpenRouter + Nous Portal).

Both OpenRouter and the Nous Portal inference endpoint speak the same
OpenAI-style ``/chat/completions`` image-generation protocol: send
``modalities: ["image", "text"]`` with an image-output model (e.g.
``google/gemini-3-pro-image``), pass reference images as ``image_url``
content parts for grounding, and read the generated images back from
``choices[0].message.images[].image_url.url`` (a ``data:image/...;base64`` URI).

Nous Portal proxies OpenRouter, so one implementation services both — we only
swap the resolved ``(base_url, api_key)``. Credentials are resolved through the
agent's existing :func:`~hermes_cli.runtime_provider.resolve_runtime_provider`,
which already understands OpenRouter's key pool and the Nous OAuth device-code
token, so this plugin never reinvents auth.

Reference grounding is the reason pet sprite generation cares about this
backend: each animation row must stay the same character as the chosen base
frame, which only works on models that accept image input. Gemini Flash Image
("nano-banana") does, so both providers advertise image-to-image support.
    )annotationsN)Path)AnyDictListOptional)DEFAULT_ASPECT_RATIOImageGenProvidererror_responseresolve_aspect_ratiosave_b64_imagesave_url_imagesuccess_responsezopenai/gpt-5.4-image-2zgoogle/gemini-3-pro-image1:1z16:9z9:16)square	landscapeportrait   g     r@returnDict[str, Any]c                    	 ddl m}   |             }t          |t                    r|                    d          nd}t          |t                    r|ni S # t
          $ r'}t                              d|           i cY d}~S d}~ww xY w)zDRead the ``image_gen`` section from config.yaml (``{}`` on failure).r   )load_config	image_genNz#could not load image_gen config: %s)hermes_cli.configr   
isinstancedictget	Exceptionloggerdebug)r   cfgsectionexcs       K/home/rurouni/.hermes/hermes-agent/plugins/image_gen/openrouter/__init__.py_load_image_gen_configr%   J   s    111111kmm*4S$*?*?I#''+&&&T$Wd33;ww;   :C@@@						s   AA 
B!B=BBrefstrOptional[str]c                   t          | pd                                          } | sdS |                     d          r| S t          |           }ddlm}  ||            	 |                                }n4# t          $ r'}t          	                    d| |           Y d}~dS d}~ww xY wt          j        |j                  d         pd}t          j        |                              d          }d	| d
| S )a4  Turn a reference (local path or http URL) into an ``image_url`` value.

    Remote URLs pass through unchanged; local files are inlined as base64 data
    URIs so the request is self-contained (the provider endpoint can't reach a
    path on our disk). Returns ``None`` when the reference can't be read.
     N)zhttp://zhttps://data:r   )raise_if_read_blockedz%could not read reference image %s: %sz	image/pngasciir+   z;base64,)r'   strip
startswithr   agent.file_safetyr,   
read_bytesOSErrorr   r    	mimetypes
guess_typenamebase64	b64encodedecode)r&   pathr,   rawr#   mimeencodeds          r$   _to_image_url_partr=   W   s    ciR..


 
 C t
~~677 
99D777777#oo   <c3GGGttttt 	**1-<Ds##**733G*4*****s    A5 5
B&?B!!B&payload	List[str]c                   g }t          | t                    r|                     d          nd}t          |t                    s|S |D ]}t          |t                    r|                    d          nd}t          |t                    r|                    d          nd}t          |t                    sq|D ]}t          |t                    s|                    d          }t          |t                    r|                    d          nd}t          |t                    r;|                                r'|                    |                                           |S )zPull generated image URLs from a chat-completions response.

    OpenRouter returns generated images under
    ``choices[0].message.images[].image_url.url`` (typically a base64 data URI).
    choicesNmessageimages	image_urlurl)r   r   r   listr'   r.   append)	r>   outrA   choicerB   rC   imagerD   rE   s	            r$   _extract_imagesrK   r   sU    C(27D(A(AKgkk)$$$tGgt$$ 
 ( (+5fd+C+CM&**Y'''*4Wd*C*CMX&&&&$'' 	 	( 	(EeT** 		+..I*4Y*E*EO)--&&&4C#s## (		 (

399;;'''	( J    displaymodel_idenv_varstatusinterr_msgc                    |                     d          sdS |pd                                |dv pt          fddD                       }|sdS |  d| d| d	|  d
| dt           dS )u  A targeted hint when an access-gated OpenAI image model can't be reached.

    Some OpenAI image models on OpenRouter need account enablement / BYOK, so the
    failure isn't a missing key (the key is valid) — the *model* is unreachable.
    The generic "check your key" message is misleading there, so we detect that
    case and point the user at the real fix. Returns one actionable line, or
    ``None`` when this isn't the access-gated case.
    zopenai/Nr*   )i  i  i  c              3      K   | ]}|v V  	d S N ).0slows     r$   	<genexpr>z%_access_error_hint.<locals>.<genexpr>   s8       - -S- - - - - -rL   )zno endpointsz
no allowedznot a valid modelzdata policyz can't reach image model 'z' (u)   ) — enable OpenAI image access in your z account, or set =.)r/   lowerany_FALLBACK_MODEL)rM   rN   rO   rP   rR   gatedrY   s         @r$   _access_error_hintra      s     y)) t=b


!
!Co%  - - - -\- - - * *E  t 	W 	Wh 	W 	W6 	W 	W '	W 	W:A	W 	WDS	W 	W 	WrL   models	list[str]c                    g }t                      }| D ]I}|pd                                }|r||v r|                    |           |                    |           J|S )Nr*   )setr.   addrG   )rb   rH   seenmodelms        r$   _dedupe_modelsrj      sl    CUUD  [b!! 	AII

1JrL   c                      e Zd ZdZd&dZed'd            Zed'd            Zd(dZd)dZ	d(dZ
d*dZd+dZd(dZd,d-dZd,d.dZefdddd/d%ZdS )0OpenRouterCompatImageProvidera  Image generation over an OpenRouter-compatible chat-completions endpoint.

    Instantiated once per backend (OpenRouter, Nous Portal). The two differ only
    in which runtime provider supplies ``(base_url, api_key)`` and in the config
    namespace used for the model override.
    provider_namer'   display_nameruntime_name
config_keymodel_env_varsetup_schemar   r   Nonec               Z    || _         || _        || _        || _        || _        || _        d S rU   )_name_display_runtime_name_config_key_model_env_var_setup_schema)selfrm   rn   ro   rp   rq   rr   s          r$   __init__z&OpenRouterCompatImageProvider.__init__   s8     #
$)%+)rL   c                    | j         S rU   )ru   r{   s    r$   r5   z"OpenRouterCompatImageProvider.name   s
    zrL   c                    | j         S rU   )rv   r~   s    r$   rn   z*OpenRouterCompatImageProvider.display_name   s
    }rL   c                0    ddl m}  || j                  S )z@Resolve ``(base_url, api_key)`` via the shared runtime resolver.r   )resolve_runtime_provider)	requested)hermes_cli.runtime_providerr   rw   )r{   r   s     r$   _resolve_runtimez.OpenRouterCompatImageProvider._resolve_runtime   s+    HHHHHH''$2DEEEErL   boolc                &   	 |                                  }n9# t          $ r,}t                              d| j        |           Y d }~dS d }~ww xY wt          t          |                    d          pd                                                    S )Nz %s runtime resolution failed: %sFapi_keyr*   )	r   r   r   r    ru   r   r'   r   r.   )r{   runtimer#   s      r$   is_availablez*OpenRouterCompatImageProvider.is_available   s    	++--GG 	 	 	LL;TZMMM55555	 CI..4"55;;==>>>s    
A!AAc                    ddgt           dS )NtextrJ   )
modalitiesmax_reference_images)_MAX_REFERENCE_IMAGESr~   s    r$   capabilitiesz*OpenRouterCompatImageProvider.capabilities   s     "7+$9
 
 	
rL   List[Dict[str, Any]]c                .    t           dddt          dddgS )NzOpenAI GPT-5.4 Image 2z=Highest fidelity; best prompt adherence; slower on OpenRouter)idrM   	strengthszGemini 3 Pro Imagez2Fast, reliable fallback with good layout adherence)DEFAULT_MODELr_   r~   s    r$   list_modelsz)OpenRouterCompatImageProvider.list_models   s6     $3\  &/Q 
 	
rL   r(   c                *    |                                  S rU   )_resolve_modelr~   s    r$   default_modelz+OpenRouterCompatImageProvider.default_model   s    ""$$$rL   c                *    t          | j                  S rU   )r   rz   r~   s    r$   get_setup_schemaz.OpenRouterCompatImageProvider.get_setup_schema   s    D&'''rL   Nexplicitc                8    |                      |          d         S )z=Pick the image model (first of :meth:`_resolve_model_chain`).r   )_resolve_model_chain)r{   r   s     r$   r   z,OpenRouterCompatImageProvider._resolve_model   s    ((22155rL   rc   c                \   t          |t                    r)|                                r|                                gS t          j                            | j        d                                          }|r|gS t                      }t          |                    | j                  t                    r|                    | j                  ni }t          |t                    rS|                    d          }t          |t                    r)|                                r|                                gS |                    d          }t          |t                    r)|                                r|                                gS t          t          t                              S )u  Ordered model attempts for this request.

        Precedence: explicit caller override (the ``model`` kwarg) → the
        provider's ``*_IMAGE_MODEL`` env override → scoped
        ``image_gen.<provider>.model`` → top-level ``image_gen.model`` (written
        by ``hermes tools``) → the quality-first default chain.

        Any explicit user/model selection means "use this exact model", so no
        fallback. Only the bare default chain carries a Gemini fallback.
        r*   rh   )r   r'   r.   osenvironr   ry   r%   rx   r   rj   rF   _DEFAULT_MODEL_CHAIN)r{   r   env_overrider!   scopedvaluetops          r$   r   z2OpenRouterCompatImageProvider._resolve_model_chain   s_    h$$ 	&)9)9 	&NN$$%%z~~d&92>>DDFF 	" >!$&&.8AQ9R9RTX.Y.Ya)***_afd## 	'JJw''E%%% '%++-- '&gggc3 	!CIIKK 	!IIKK= d#788999rL   )rD   reference_image_urlspromptaspect_ratiorD   r   Optional[List[str]]kwargsr   c          
        dd l }	 |                                 }n:# t          $ r-}t          d| j         d| d| j        |          cY d }~S d }~ww xY wt          |                    d          pd                                          }	t          |                    d          pd                                          	                    d	          }
|	r|
s)t          d
| j         d| j         dd| j        |          S | 
                    |                    d                    }t          |          }t                              |d          }g }|                    d          pg D ]$}|                    t          |                     %|r"|                    t          |                     |pg D ]$}|                    t          |                     %d|dg}|d t                   D ]-}t          |          }|r|                    dd|id           .d|	 dddd}d }t!          |          D ]\  }}|ddgd|dgd|id}|t#          |          dz
  k    }	 |                    |
 d ||t&          !          }|                                 n# |j        $ r=}|j        }||j        nd}	 |                                                    d"i                               d#|j        d d$                   }n0# t          $ r# ||j        d d$         nt          |          }Y nw xY wt4                              d%| j        |||           t9          | j        || j        ||          }|r3|s1t4                              d&| j        |||dz                       Y d }~pt          |p| j         d'| d(| |rd)nd*| j        |||+          }|cY d }~c S d }~w|j        $ rl |s.t4                              d,| j        |||dz                       Y t          | j         d-tA          t&                     d.d/| j        |||+          cY c S |j!        $ r0}t          | j         d0| d1| j        |||+          cY d }~c S d }~ww xY w	 |                                }n=# t          $ r0}t          | j         d2| d3| j        |||+          cY d }~c S d }~ww xY wtE          |          }|sV|s-t4                              d4| j        |||dz                       t          | j         d5| d6d7| j        |||+          c S |d         }	 |#                    d8          r<d9|v r|$                    d9d          d         nd} tK          | | j         d:;          }!ntM          || j         d:;          }!n6# t          $ r)}t          d<| d=| j        |||+          cY d }~c S d }~ww xY wtO          t          |!          |||| j        >          c S |p+t          | j         d?d*| j        |r|d@         nd||+          S )ANr   zCould not resolve z credentials: missing_api_key)error
error_typeproviderr   r   r*   base_url/zNo z credentials found. Configure u(    in `hermes tools` → Image Generation.rh   r   reference_imagesr   )typer   rD   rE   )r   rD   zBearer zapplication/jsonz,https://github.com/NousResearch/hermes-agentzHermes Agent)AuthorizationzContent-TypezHTTP-RefererzX-TitlerJ   user)rolecontentr   )rh   r   messagesimage_config   z/chat/completions)headersjsontimeoutr   rB   i,  z"%s image gen failed (%d) on %s: %sz2%s model %s unavailable; retrying with fallback %sz image generation failed (z): model_access	api_error)r   r   r   rh   r   r   z0%s model %s timed out; retrying with fallback %sz image generation timed out (zs)r   z connection error: connection_errorz returned invalid JSON: invalid_responsez8%s model %s returned no image; retrying with fallback %sz& returned no image. Ensure the model 'z' supports image output.empty_responser+   ,_gen)prefixz Could not save generated image: io_error)rJ   rh   r   r   r   z; image generation failed after trying all candidate models.)(requestsr   r   r   rv   ru   r'   r   r.   rstripr   r   _ASPECT_RATIOSrG   r   r=   	enumeratelenpost_REQUEST_TIMEOUTraise_for_status	HTTPErrorresponsestatus_coder   r   r   r   ra   ry   infoTimeoutrQ   ConnectionErrorrK   r/   splitr   r   r   )"r{   r   r   rD   r   r   r   r   r#   r   r   model_chainaspect	or_aspect
referencesr&   r   partr   
last_errorirN   r>   is_lastr   resprP   rR   hintresultrC   firstb64
saved_paths"                                     r$   generatez&OpenRouterCompatImageProvider.generate  s	    		++--GG 	 	 	!M4=MMMM,)	        	 gkk),,23399;;w{{:..4"55;;==DDSII 		h 		!Y$- Y Y!%Y Y Y -)    //

70C0CDD%l33"&&vu55	
 !#
::0117R 	( 	(Cc#hh'''' 	.c)nn---'-2 	( 	(Cc#hh''''28&)I)I(J4445 	R 	RC%c**D R5$-PPQQQ 1w00.J%
 
 04
$[11 A	 A	KAx!&/&,AAB!/ ;	' 'G 3{++a//G<#==222# ,	 )   ))++++% " " "|-1-=))1P"iikkoogr::>>y$)TXUXTX/ZZGG  P P P151Adioos3xxGGGPA4:vW_ahiii)$-4CVX^`ghh  KKL
 #AE*	   HHHH+bT]"b"bf"b"bY`"b"b15F~~;!Z"!!'  
 "!!!!!!!!#    KKJ
 #AE*	   H%!] 2 2,--2 2 2(!Z"!!'        +   %!]DDsDD1!Z"!!'          
!   %!]IICII1!Z"!!'           %V,,F  KKR
 #AE*	    &= ? ?$? ? ?  0!Z"!!'
 
 
 
 
 
 1IE##G,, S475LL%++c1--a00bC!/tz<O<O<O!P!P!PJJ!/>Q>Q>Q!R!R!RJ   %BSBB)!Z"!!'           $*oo#       
^]___"Z%08+b//b
 
 
 	
s    
A"AAA(5JR*O"=A	LO"*L41O"3L44A2O",.O"R":R7R	R##RRRR,,
S&6#S!S&!S&A*W
W7W2*W72W7)rm   r'   rn   r'   ro   r'   rp   r'   rq   r'   rr   r   r   rs   )r   r'   r   r   )r   r   )r   r   )r   r(   rU   )r   r(   r   r'   )r   r(   r   rc   )r   r'   r   r'   rD   r(   r   r   r   r   r   r   )__name__
__module____qualname____doc__r|   propertyr5   rn   r   r   r   r   r   r   r   r   r	   r   rV   rL   r$   rl   rl      sY        * * * *"    X    XF F F F? ? ? ?
 
 
 

 
 
 
% % % %( ( ( (6 6 6 6 6: : : : :< 1H

 $(48H
 H
 H
 H
 H
 H
 H
 H
rL   rl   #List[OpenRouterCompatImageProvider]c                 v    t          ddddddddddd	d
gd          t          ddddddddg dd          gS )N
openrouter
OpenRouterOPENROUTER_IMAGE_MODELzOpenRouter (image)paidzAGemini Flash Image & more via OpenRouter; uses OPENROUTER_API_KEYOPENROUTER_API_KEYzOpenRouter API keyzhttps://openrouter.ai/keys)keyr   rE   )r5   badgetagenv_vars)rm   rn   ro   rp   rq   rr   nouszNous PortalNOUS_IMAGE_MODELzNous Portal (image)subscriptionzGReference-grounded image generation via Nous Portal (OpenRouter-backed)T)r5   r   r   r   requires_nous_auth)rl   rV   rL   r$   _build_providersr     s    %&%%#2,Z  4"6; 	 	
 	
 	
& 	& &,-'`&* 	
 	
 	
)" "rL   ctxr   rs   c                R    t                      D ]}|                     |           dS )z:Register the OpenRouter + Nous Portal image gen providers.N)r   register_image_gen_provider)r   r   s     r$   registerr     s8    $&& 2 2''11112 2rL   r   )r&   r'   r   r(   )r>   r   r   r?   )rM   r'   rN   r'   rO   r'   rP   rQ   rR   r'   r   r(   )rb   rc   r   rc   )r   r   )r   r   r   rs   )'r   
__future__r   r6   loggingr3   r   pathlibr   typingr   r   r   r   agent.image_gen_providerr	   r
   r   r   r   r   r   	getLoggerr   r   r   r_   r   r   r   r   r%   r=   rK   ra   rj   rl   r   r   rV   rL   r$   <module>r     s   * # " " " " "       				       , , , , , , , , , , , ,                  
	8	$	$ )-%7 
    
  
 
 
 
+ + + +6   2   2	 	 	 	r
 r
 r
 r
 r
$4 r
 r
 r
j	# # # #L2 2 2 2 2 2rL   