
     j$                        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m	Z	m
Z
 ddlmZmZ  ej                  e      Z G d de      ZddZd	 Zdd
Zdddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZy)u  OpenRouter VLM 판정 클라이언트 — QK 판정 체계 경로 (2026-08-12).

`backend/build_qk_openrouter_eval.py` 재평가에서 검증된 부품의 프로덕션
이관이다. Router(`call_structured`)를 타지 않는 이유는 qwen_vlm_client 와
같다 — 이 저장소의 구조화 호출은 `json_schema`/`strict` 를 보내는데
OpenRouter 경유 모델(qwen/kimi)은 `json_object` 까지만 신뢰할 수 있고,
모델·배포본에 따라 `response_format` 자체를 거부(400)하기도 한다. 그래서
스키마 전문을 시스템에 동봉하고 여기서 `jsonschema` 로 검증한 뒤, 실패하면
오류를 되먹여 한 번 다시 묻는다 (Qwen 클라이언트와 같은 계약).

★litellm 우회 직접 호출이므로 **모든 API 왕복을 `record_provider_call` 로
남긴다** (llm_call_log+Opik — 2026-08-07 전수 기록 지시). 우회 호출 AST
스캐너(`test_all_provider_calls_are_traced`)가 이 배선을 검사한다.
    )annotationsN)AnyDictListOptional)JSON_CLAUSEextract_jsonc                      e Zd Zy)OpenRouterNotConfiguredN)__name__
__module____qualname__     [/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/openrouter_vlm_client.pyr   r      s    r   r   c                 \    ddl m}  t        | j                  xs dj	                               S )Nr   settings )app.core.configr   boolopenrouter_api_keystripr   s    r   openrouter_configuredr      s$    (,,299;<<r   c                     ddl m}  | j                  xs dj                         }|st	        d      ddlm}  ||| j                  xs dj                         d      S )Nr   r   r   uK   OPENROUTER_API_KEY 가 비어 있다 — backend/.env 에 키를 넣어라.)OpenAIg     r@)api_keybase_urltimeout)r   r   r   r   r   openair   openrouter_base_url)r   keyr   s      r   _clientr#   %   s`    (&&,"
3
3
5C%Y
 	
 ..4";;= r   c                2    dj                  d | D              S )ud   기록용 프롬프트 — 텍스트 파트만 잇는다 (base64 이미지는 DB 에 안 싣는다).
c              3     K   | ]7  }|j                  d       dk(  st        |j                  d      xs d       9 yw)typetextr   N)getstr).0ps     r   	<genexpr>z"_prompt_text_of.<locals>.<genexpr>8   s5      */Q155=F3JAEE&MR %s   ?#?)join)partss    r   _prompt_text_ofr0   6   s     99 */  r      )	max_retry
max_tokensopik_metadatac          
     D    ddl m}m}	 ddlm}
mm}m t               }|t        j                  t        j                  |dd            z   }d|d	d
|d	g}t         |
             |xs i j                         D ]  \  }}|	|vs||<     |       d| dt        |       dddd	 	 	 	 	 d, fd}d}t!        |dz         D ]  }d}t#        j"                         }ddidfD ]O  }t#        j"                         }	  |j$                  j&                  j(                  d-|d|rd|ini |rd|ini } n |t1        d      t-        t#        j"                         |z
  dz        }	 |j2                  d   j4                  j6                  xs d}t9        |t:              s!t=        dt?        |      j@                         	 	 tC        |      } |	||        |d"|||#       |c S  t1        d+      # t*        $ r`}t-        t#        j"                         |z
  dz        }|dt/        |      vr |d|t/        |      dd          |d|d       Y d}~qd}~ww xY w# t*        $ r} |d||d|dd  !        d}~ww xY w# tD        |t        jF                  f$ rQ}t?        |      j@                   d$| dd  } |d||d%| !       ||k\  r |d&|dd' d	d
d(| d)d	gz  }Y d}~#d}~wt*        $ r} |d||d*|dd  !        d}~ww xY w).uo  OpenRouter 모델에 멀티모달 판정을 묻고 스키마 검증 payload 를 돌려준다.

    parts 는 `call_structured` 멀티모달 parts 와 같은 형식(`png_part`/
    `ref_parts` 산출 — OpenAI chat `image_url` data URI). 계약은
    `ask_qwen_structured` 와 동형 — client·모델 슬롯만 다르다.

    `response_format={"type":"json_object"}` 를 우선 시도하고, 모델이 그
    파라미터 자체를 거부(400)하면 빼고 한 번 더 시도한다(재평가 실측 계약).
    스키마 검증 실패 = 오류를 되먹여 교정 재질의 `max_retry` 회.

    max_tokens (2026-08-13 G+G46): 긴 구조화 출력의 미달-잘림 방지용 출력
    예산 — 파일럿 실측에서 판정류 출력이 기본치에 잘려 따옴표 파손으로
    죽었다(qwen 4000 잘림 14건 동류). None(default)=미전송 byte-identical.
    r   )ValidationErrorvalidate)ambient_call_metarecord_provider_callresolve_step_nameusage_of_openai_responseF   )ensure_asciiindent)schemasystem)rolecontentuserNz	[system]
z	

[user]
)outputerrorresprE   c               L    | |      ni } 	
| |d|||xs d        y )N
openrouter)stepmodelpromptstatusduration_msmetaprovider	operationoutput_textrE   usager   )rL   dur_msrD   rE   rF   rR   
log_promptrN   rJ   r9   rI   step_tagr;   s         r   _recordz*ask_openrouter_structured.<locals>._recordu   s=     372B(.U:fTLF%-4		!r   r   r1   r'   json_object)rJ   messagesresponse_formatr3   i  400i  )rE   u3   response_format 미지원(400) — 빼고 재시도u@   openrouter 호출이 응답 없이 폴백 루프를 벗어났다u'   message.content 가 str 이 아니다: zunexpected response shape: i  )rF   rE   success)rD   rF   z: zschema violation: 	assistanti  z,That reply did not satisfy the json schema: z'
Return the corrected json object only.z"unexpected post-response failure: unreachable)rL   r*   rS   intrD   Optional[str]rE   r_   rF   r   returnNoner   )$
jsonschemar6   r7   app.modules.llm.image_tracerr8   r9   r:   r;   r#   r   formatjsondumpsdictitemsr0   rangetimechatcompletionscreate	Exceptionr^   reprRuntimeErrorchoicesmessagerB   
isinstancer*   	TypeErrorr'   r   r	   
ValueErrorJSONDecodeError) rU   r@   r/   r?   rJ   r2   r3   r4   r6   _js_validater8   r:   client
sys_promptrX   kvrV   last_errattemptrF   t0rfexcdurrB   payloadrT   rN   r9   rI   r;   s    `   `                      @@@@@r   ask_openrouter_structuredr   =   s   2 E" " YF+,,zz&uQ?A AJ j1E*&H !#$D$"++-1=Qd]DG . Xt,D zl +,U346J FJ'+	!$	!36	!BF	! 	! HQ'YY[ M*D1BBU5v{{..55 % 35)2." 6@j1R	  2" <abb499;#t+,	ll1o--55;Ggs+=G}--.01 1 ,	"7+G&)Is7>N] (P }
%%s  U499;+t34:d3i!7GSS	$3@SU UU*  	 GSt7w?EG	 OT-A-AB 	s),,-Ru5ds;HGSt.xj9;)# $$@Bj HJ H  		 GSt>sgFtLN		sW   7H;AI1!J	I.AI))I.1	J:JJL/AK;;LLL)r`   r   )r/   List[Dict[str, Any]]r`   r*   )rU   r*   r@   r*   r/   r   r?   Dict[str, Any]rJ   r*   r2   r^   r3   zOptional[int]r4   zOptional[Dict[str, Any]]r`   r   )__doc__
__future__r   re   loggingrj   typingr   r   r   r   app.modules.llm.qwen_vlm_clientr   r	   	getLoggerr   loggerrp   r   r   r#   r0   r   r   r   r   <module>r      s    #    , , E			8	$	l 	="  $.2L&L&L&  L& 	L& L& L& L& ,L& L&r   