|
43 | 43 | ClientConfig, |
44 | 44 | FinishReason, |
45 | 45 | Message, |
| 46 | + MessageContent, |
46 | 47 | Messages, |
47 | 48 | Response, |
48 | 49 | ResponseMessage, |
@@ -119,6 +120,14 @@ def content_to_text(content: Any) -> str: |
119 | 120 | return "" |
120 | 121 |
|
121 | 122 |
|
| 123 | +def parse_refusal_content(message: Any) -> str | None: |
| 124 | + if isinstance(message, Mapping): |
| 125 | + refusal = message.get("refusal") |
| 126 | + else: |
| 127 | + refusal = getattr(message, "refusal", None) |
| 128 | + return refusal if isinstance(refusal, str) and refusal else None |
| 129 | + |
| 130 | + |
122 | 131 | DEFAULT_REASONING_FIELDS = [ |
123 | 132 | "reasoning", # vLLM, Together AI, OpenRouter |
124 | 133 | "reasoning_content", # DeepSeek, Qwen/DashScope, SGLang, Fireworks AI, Kimi/Moonshot |
@@ -331,15 +340,29 @@ async def raise_from_native_response(self, response: OpenAIChatResponse) -> None |
331 | 340 | f"Model returned {len(response.choices)} choices, expected 1" |
332 | 341 | ) |
333 | 342 | message = response.choices[0].message |
334 | | - has_content = bool(content_to_text(getattr(message, "content", None))) |
| 343 | + has_content = bool( |
| 344 | + content_to_text(getattr(message, "content", None)) |
| 345 | + or parse_refusal_content(message) |
| 346 | + ) |
335 | 347 | has_tool_calls = bool(getattr(message, "tool_calls", None)) |
336 | 348 | has_reasoning = bool(parse_reasoning_content(message)) |
337 | | - if not (has_content or has_tool_calls or has_reasoning): |
| 349 | + if not (has_content or has_tool_calls): |
| 350 | + if has_reasoning: |
| 351 | + raise EmptyModelResponseError( |
| 352 | + "Model returned reasoning but no content and did not call any tools" |
| 353 | + ) |
338 | 354 | raise EmptyModelResponseError( |
339 | | - "Model returned no content, reasoning, and did not call any tools" |
| 355 | + "Model returned no content and did not call any tools" |
340 | 356 | ) |
341 | 357 |
|
342 | 358 | async def from_native_response(self, response: OpenAIChatResponse) -> Response: |
| 359 | + def parse_content(response: OpenAIChatResponse) -> MessageContent | None: |
| 360 | + message = response.choices[0].message |
| 361 | + content = message.content |
| 362 | + if content_to_text(content): |
| 363 | + return content |
| 364 | + return parse_refusal_content(message) |
| 365 | + |
343 | 366 | def parse_single_tool_call(tool_call: Any) -> ToolCall | None: |
344 | 367 | if isinstance(tool_call, ChatCompletionMessageFunctionToolCall): |
345 | 368 | return ToolCall( |
@@ -511,7 +534,7 @@ def parse_tokens(response: OpenAIChatResponse) -> ResponseTokens | None: |
511 | 534 | model=model, |
512 | 535 | usage=parse_usage(response), |
513 | 536 | message=ResponseMessage( |
514 | | - content=response.choices[0].message.content, |
| 537 | + content=parse_content(response), |
515 | 538 | reasoning_content=parse_reasoning_content(response.choices[0].message), |
516 | 539 | finish_reason=parse_finish_reason(response), |
517 | 540 | is_truncated=parse_is_truncated(response), |
|
0 commit comments