
    `gjP                    *   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$ddZ%ddZ&y)u  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@c                     	 ddl m}   |        }t        |t              r|j	                  d      nd}t        |t              r|S i S # t
        $ r"}t        j                  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       X/root/.hermes/venv/lib/python3.12/site-packages/plugins/image_gen/openrouter/__init__.py_load_image_gen_configr#   J   sb    1m*4S$*?#''+&T$Wd3w;; :C@	s$   AA A 	A1A,&A1,A1c                   t        | xs d      j                         } | sy| j                  d      r| S t        |       }ddlm}  ||        	 |j                         }t        j                  |j                        d   xs d}t        j                  |      j                  d      }d	| d
| S # t        $ r!}t        j                  d| |       Y d}~yd}~ww xY w)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,)strstrip
startswithr   agent.file_safetyr'   
read_bytesOSErrorr   r   	mimetypes
guess_typenamebase64	b64encodedecode)refpathr'   rawr!   mimeencodeds          r"   _to_image_url_partr:   W   s     ciR.


 C
~~67
9D7#oo 		*1-<Ds#**73G4&	**  <c3Gs   B0 0	C9CCc                J   g }t        | t              r| j                  d      nd}t        |t              s|S |D ]  }t        |t              r|j                  d      nd}t        |t              r|j                  d      nd}t        |t              sZ|D ]  }t        |t              s|j                  d      }t        |t              r|j                  d      nd}t        |t              sY|j                         sj|j                  |j                                  |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)	payloadoutr<   choicer=   r>   imager?   r@   s	            r"   _extract_imagesrG   r   s     C(27D(Agkk)$tGgt$
 (+5fd+C&**Y'*4Wd*CX&&$' 	(EeT*		+.I*4Y*E)--&4C#s#		

399;'	(( J    c                    |j                  d      sy|xs dj                         |dv xs t        fddD              }|sy|  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  
 y wN ).0slows     r"   	<genexpr>z%_access_error_hint.<locals>.<genexpr>   s      -S-s   )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)displaymodel_idenv_varstatuserr_msggatedrO   s         @r"   _access_error_hintr\      s     y)=b


!Co%  -\- *E )-hZs6( C  'y(9'!OCTTU	WrH   c                    g }t               }| D ]?  }|xs dj                         }|r||v r|j                  |       |j                  |       A |S )Nr%   )setr*   addrB   )modelsrD   seenmodelms        r"   _dedupe_modelsrd      sX    CUD [b!AI

1 JrH   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y)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.
    c               X    || _         || _        || _        || _        || _        || _        y rK   )_name_display_runtime_name_config_key_model_env_var_setup_schema)selfprovider_namedisplay_nameruntime_name
config_keymodel_env_varsetup_schemas          r"   __init__z&OpenRouterCompatImageProvider.__init__   s2     #
$)%+)rH   c                    | j                   S rK   )rh   rn   s    r"   r1   z"OpenRouterCompatImageProvider.name   s    zzrH   c                    | j                   S rK   )ri   rw   s    r"   rp   z*OpenRouterCompatImageProvider.display_name   s    }}rH   c                4    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_providerrz   rj   )rn   rz   s     r"   _resolve_runtimez.OpenRouterCompatImageProvider._resolve_runtime   s    H'$2D2DEErH   c                    	 | j                         }t        t        |j                  d      xs d      j                               S # t        $ r+}t        j                  d| j                  |       Y d }~yd }~ww xY w)Nz %s runtime resolution failed: %sFapi_keyr%   )	r}   r   r   r   rh   boolr)   r   r*   )rn   runtimer!   s      r"   is_availablez*OpenRouterCompatImageProvider.is_available   sh    	++-G CI.4"5;;=>>  	LL;TZZM	s   A 	A;!A66A;c                    ddgt         dS )NtextrF   )
modalitiesmax_reference_images)_MAX_REFERENCE_IMAGESrw   s    r"   capabilitiesz*OpenRouterCompatImageProvider.capabilities   s     "7+$9
 	
rH   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)idrV   	strengthszGemini 3 Pro Imagez2Fast, reliable fallback with good layout adherence)DEFAULT_MODELrU   rw   s    r"   list_modelsz)OpenRouterCompatImageProvider.list_models   s,     $3\ &/Q
 	
rH   c                "    | j                         S rK   )_resolve_modelrw   s    r"   default_modelz+OpenRouterCompatImageProvider.default_model   s    ""$$rH   c                ,    t        | j                        S rK   )r   rm   rw   s    r"   get_setup_schemaz.OpenRouterCompatImageProvider.get_setup_schema   s    D&&''rH   Nc                *    | j                  |      d   S )z=Pick the image model (first of :meth:`_resolve_model_chain`).r   )_resolve_model_chain)rn   explicits     r"   r   z,OpenRouterCompatImageProvider._resolve_model   s    ((2155rH   c                   t        |t              r!|j                         r|j                         gS t        j                  j                  | j                  d      j                         }|r|gS t               }t        |j                  | j                        t              r|j                  | j                        ni }t        |t              rB|j                  d      }t        |t              r!|j                         r|j                         gS |j                  d      }t        |t              r!|j                         r|j                         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%   rb   )r   r)   r*   osenvironr   rl   r#   rk   r   rd   rA   _DEFAULT_MODEL_CHAIN)rn   r   env_overrider   scopedvaluetops          r"   r   z2OpenRouterCompatImageProvider._resolve_model_chain   s    h$)9NN$%%zz~~d&9&92>DDF >!$&.8AQAQ9RTX.Y))*_afd#JJw'E%%%++-&gggc3CIIKIIK= d#7899rH   )r?   reference_image_urlsc          
        dd l }	 | j                         }t        |j                  d      xs d      j                         }	t        |j                  d      xs d      j                         j                  d	      }
|	r|
s4t        d
| j                   d| j                   dd| j
                  |      S | j                  |j                  d            }t        |      }t        j                  |d      }g }|j                  d      xs g D ]  }|j                  t        |              |r|j                  t        |             |xs g D ]  }|j                  t        |              d|dg}|d t         D ]&  }t        |      }|s|j                  dd|id       ( d|	 dddd}d }t!        |      D ]S  \  }}|ddgd|dgd|id}|t#        |      dz
  k(  }	 |j%                  |
 d ||t&        !      }|j)                          	 |j1                         }tE        |      }|sX|s)t4        j=                  d4| j
                  |||dz             t        | j                   d5| d6d7| j
                  |||+      c S |d   }	 |jG                  d8      r6d9|v r|jI                  d9d      d   nd} tK        | | j
                   d:;      }!ntM        || j
                   d:;      }!tO        t        |!      |||| j
                  >      c S  |xs/ t        | j                   d?d*| j
                  |r|d@   nd||+      S # t        $ r3}t        d| j                   d| d| j
                  |      cY d }~S d }~ww xY w# |j*                  $ rA}|j,                  }||j.                  nd}	 |j1                         j                  d"i       j                  d#|j2                  d d$       }n+# t        $ r ||j2                  d d$ n
t        |      }Y nw xY wt4        j7                  d%| j
                  |||       t9        | j                  || j:                  ||      }|r0|s.t4        j=                  d&| j
                  |||dz             Y d }~t        |xs | j                   d'| d(| |rd)nd*| j
                  |||+      }|cY d }~c S d }~w|j>                  $ rj |s+t4        j=                  d,| j
                  |||dz             Y Tt        | j                   d-tA        t&               d.d/| j
                  |||+      cY c S |jB                  $ r6}t        | j                   d0| d1| j
                  |||+      cY d }~c S d }~ww xY w# t        $ r6}t        | j                   d2| d3| j
                  |||+      cY d }~c S d }~ww xY w# t        $ r*}t        d<| d=| j
                  |||+      cY d }~c S d }~ww xY w)ANr   zCould not resolve z credentials: missing_api_key)error
error_typeprovideraspect_ratior   r%   base_url/zNo z credentials found. Configure u(    in `hermes tools` → Image Generation.rb   r   reference_imagesr   )typer   r?   r@   )r   r?   zBearer zapplication/jsonz,https://github.com/NousResearch/hermes-agentzHermes Agent)AuthorizationzContent-TypezHTTP-RefererzX-TitlerF   user)rolecontentr   )rb   r   messagesimage_config   z/chat/completions)headersjsontimeoutr   r=   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   rb   promptr   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)rF   rb   r   r   r   z; image generation failed after trying all candidate models.)(requestsr}   r   r   ri   rh   r)   r   r*   rstripr   r   _ASPECT_RATIOSrB   r   r:   	enumeratelenpost_REQUEST_TIMEOUTraise_for_status	HTTPErrorresponsestatus_coder   r   r   r   r\   rl   infoTimeoutintConnectionErrorrG   r+   splitr   r   r   )"rn   r   r   r?   r   kwargsr   r   r!   r   r   model_chainaspect	or_aspect
referencesr5   r   partr   
last_errorirW   rC   is_lastr   resprY   rZ   hintresultr>   firstb64
saved_paths"                                     r"   generatez&OpenRouterCompatImageProvider.generate  su    		++-G gkk),2399;w{{:.4"5;;=DDSIh!$-- )!!%/WY -)  //

70CD%l3"&&vu5	
 !#
::017R 	(Cc#h'	(c)n-'-2 	(Cc#h'	( 39&)I(J445 	RC%c*D5$-PQ	R  'wi0.J%
 04
$[1 A	KAx!&/&,AB!/ ;	'G 3{+a//G<#==j 12# ,	 )  ))+n
! %V,FKKR

 #AE*	  &==/ *$:%=?  0!ZZ"!!'
 
 1IE##G,475L%++c1-a0bC!/tzzl$<O!PJ!/D>Q!RJ $*o# wA	F  
^]]O#^_"ZZ%0+b/b
 	
i  	!*4==/uM,)	 	B %% "||-1-=))1P"iikoogr:>>y$))TXUX/ZG  P151Adiios3xGPA4::vW_ahi)$--4CVCVX^`ghKKL

 #AE*	 +bT]]O3MfXUXY`Xa"b15~;!ZZ"!!'
 "!## KKJ

 #AE*	 %!]]O ,,-.b2(!ZZ"!!'  ++ %!]]O+>seD1!ZZ"!!'   %!]]O+CC5I1!ZZ"!!' R  %<SEB)!ZZ"!!' s   M $,NVA!W	N(M=7N=NVS2=O0/S0%PSPA5S6S	V<V8VV)VVV	W)WWW	XX;XX)ro   r)   rp   r)   rq   r)   rr   r)   rs   r)   rt   Dict[str, Any]returnNone)r   r)   r   r   )r   r   )r   zList[Dict[str, Any]])r   Optional[str]rK   )r   r   r   r)   )r   r   r   	list[str])r   r)   r   r)   r?   r   r   zOptional[List[str]]r   r   r   r   )__name__
__module____qualname____doc__ru   propertyr1   rp   r}   r   r   r   r   r   r   r   r	   r   rL   rH   r"   rf   rf      s    * * 	*
 * * * %* 
*"    F?

%(6:< 1H

 $(48H
H
 H

 !H
 2H
 H
 
H
rH   rf   c                 f    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   r@   )r1   badgetagenv_vars)ro   rp   rq   rr   rs   rt   nouszNous PortalNOUS_IMAGE_MODELzNous Portal (image)subscriptionzGReference-grounded image generation via Nous Portal (OpenRouter-backed)T)r1   r   r   r   requires_nous_auth)rf   rL   rH   r"   _build_providersr     ss    %&%%#2,Z  4"6;		
& 	& &,-'`&*	
)" "rH   c                D    t               D ]  }| j                  |        y)z:Register the OpenRouter + Nous Portal image gen providers.N)r   register_image_gen_provider)ctxr   s     r"   registerr     s"    $& 2''12rH   r   )r5   r)   r   r   )rC   r   r   z	List[str])rV   r)   rW   r)   rX   r)   rY   r   rZ   r)   r   r   )r`   r   r   r   )r   z#List[OpenRouterCompatImageProvider])r   r   r   r   )'r   
__future__r   r2   loggingr/   r   pathlibr   typingr   r   r   r   agent.image_gen_providerr	   r
   r   r   r   r   r   	getLoggerr   r   r   rU   r   r   r   r   r#   r:   rG   r\   rd   rf   r   r   rL   rH   r"   <module>r     s   * #    	  , ,   
		8	$ )-%7 
   
  
+62*-7:EH2	r
$4 r
j	#L2rH   