@@ -21,6 +21,10 @@ class LLMHTTPError(LLMAdapterError):
2121 pass
2222
2323
24+ class LLMResponseFormatError (LLMAdapterError ):
25+ pass
26+
27+
2428@dataclass (frozen = True )
2529class LLMMessage :
2630 role : str
@@ -163,7 +167,7 @@ def complete(self, request: LLMRequest) -> LLMResponse:
163167 payload ,
164168 timeout = self .timeout ,
165169 )
166- content = response . get ( 'choices' , [{}])[ 0 ]. get ( 'message' , {}). get ( 'content' , '' )
170+ content = _extract_openai_content ( self . provider , response )
167171 usage = response .get ('usage' , {})
168172 return LLMResponse (
169173 provider = self .provider ,
@@ -201,7 +205,7 @@ def _complete_streaming(
201205 if usage :
202206 input_tokens = usage .get ('prompt_tokens' , 0 )
203207 output_tokens = usage .get ('completion_tokens' , 0 )
204- delta = parsed . get ( 'choices' , [{}])[ 0 ]. get ( 'delta' , {} )
208+ delta = _first_choice_delta ( self . provider , parsed )
205209 chunk = delta .get ('content' , '' )
206210 if chunk and request .on_chunk :
207211 request .on_chunk (chunk )
@@ -266,12 +270,7 @@ def complete(self, request: LLMRequest) -> LLMResponse:
266270 payload ,
267271 timeout = self .timeout ,
268272 )
269- content_blocks = response .get ('content' , [])
270- content = '' .join (
271- block .get ('text' , '' )
272- for block in content_blocks
273- if block .get ('type' ) == 'text'
274- )
273+ content = _extract_claude_content (response )
275274 usage = response .get ('usage' , {})
276275 return LLMResponse (
277276 provider = self .provider ,
@@ -318,8 +317,7 @@ def complete(self, request: LLMRequest) -> LLMResponse:
318317 payload ,
319318 timeout = self .timeout ,
320319 )
321- parts = response .get ('candidates' , [{}])[0 ].get ('content' , {}).get ('parts' , [])
322- content = '' .join (part .get ('text' , '' ) for part in parts )
320+ content = _extract_gemini_content (response )
323321 metadata = response .get ('usageMetadata' , {})
324322 return LLMResponse (
325323 provider = self .provider ,
@@ -331,6 +329,73 @@ def complete(self, request: LLMRequest) -> LLMResponse:
331329 )
332330
333331
332+ def _extract_openai_content (provider : str , response : dict [str , Any ]) -> str :
333+ choices = response .get ('choices' )
334+ if not isinstance (choices , list ) or not choices :
335+ raise LLMResponseFormatError (f'{ provider } response missing choices' )
336+ first_choice = choices [0 ]
337+ if not isinstance (first_choice , dict ):
338+ raise LLMResponseFormatError (f'{ provider } response choice is not an object' )
339+ message = first_choice .get ('message' )
340+ if not isinstance (message , dict ):
341+ raise LLMResponseFormatError (f'{ provider } response missing message' )
342+ content = message .get ('content' )
343+ if not isinstance (content , str ) or not content :
344+ raise LLMResponseFormatError (f'{ provider } response missing text content' )
345+ return content
346+
347+
348+ def _first_choice_delta (provider : str , response : dict [str , Any ]) -> dict [str , Any ]:
349+ choices = response .get ('choices' )
350+ if not isinstance (choices , list ) or not choices :
351+ raise LLMResponseFormatError (f'{ provider } stream chunk missing choices' )
352+ first_choice = choices [0 ]
353+ if not isinstance (first_choice , dict ):
354+ raise LLMResponseFormatError (f'{ provider } stream choice is not an object' )
355+ delta = first_choice .get ('delta' , {})
356+ if not isinstance (delta , dict ):
357+ raise LLMResponseFormatError (f'{ provider } stream delta is not an object' )
358+ return delta
359+
360+
361+ def _extract_claude_content (response : dict [str , Any ]) -> str :
362+ content_blocks = response .get ('content' )
363+ if not isinstance (content_blocks , list ):
364+ raise LLMResponseFormatError ('claude response missing content blocks' )
365+ text_parts = [
366+ block .get ('text' , '' )
367+ for block in content_blocks
368+ if isinstance (block , dict ) and block .get ('type' ) == 'text'
369+ ]
370+ content = '' .join (part for part in text_parts if isinstance (part , str ))
371+ if not content :
372+ raise LLMResponseFormatError ('claude response missing text content' )
373+ return content
374+
375+
376+ def _extract_gemini_content (response : dict [str , Any ]) -> str :
377+ candidates = response .get ('candidates' )
378+ if not isinstance (candidates , list ) or not candidates :
379+ raise LLMResponseFormatError ('gemini response missing candidates' )
380+ first_candidate = candidates [0 ]
381+ if not isinstance (first_candidate , dict ):
382+ raise LLMResponseFormatError ('gemini candidate is not an object' )
383+ content = first_candidate .get ('content' )
384+ if not isinstance (content , dict ):
385+ raise LLMResponseFormatError ('gemini response missing content' )
386+ parts = content .get ('parts' )
387+ if not isinstance (parts , list ):
388+ raise LLMResponseFormatError ('gemini response missing parts' )
389+ text = '' .join (
390+ part .get ('text' , '' )
391+ for part in parts
392+ if isinstance (part , dict ) and isinstance (part .get ('text' ), str )
393+ )
394+ if not text :
395+ raise LLMResponseFormatError ('gemini response missing text content' )
396+ return text
397+
398+
334399PROVIDER_CONFIGS = {
335400 'claude' : ProviderConfig (
336401 name = 'claude' ,
0 commit comments