|
48 | 48 | logger = logging.getLogger(__name__) |
49 | 49 |
|
50 | 50 |
|
| 51 | +def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str: |
| 52 | + """构建语义拒绝的请求体诊断上下文. |
| 53 | +
|
| 54 | + 在 semantic rejection 日志中附加请求体的可疑参数快照, |
| 55 | + 用于定位供应商参数校验失败的具体祸根参数。 |
| 56 | + """ |
| 57 | + parts: list[str] = [] |
| 58 | + # 顶层不兼容参数 |
| 59 | + for key in ("thinking", "extended_thinking", "reasoning_effort"): |
| 60 | + if key in body: |
| 61 | + val = body[key] |
| 62 | + parts.append(f"{key}={val!r:.80}") |
| 63 | + # 会话历史中的 thinking blocks |
| 64 | + thinking_count = 0 |
| 65 | + for msg in body.get("messages", []): |
| 66 | + content = msg.get("content") |
| 67 | + if not isinstance(content, list): |
| 68 | + continue |
| 69 | + for block in content: |
| 70 | + if isinstance(block, dict) and block.get("type") in ( |
| 71 | + "thinking", |
| 72 | + "redacted_thinking", |
| 73 | + ): |
| 74 | + thinking_count += 1 |
| 75 | + if thinking_count: |
| 76 | + parts.append(f"thinking_blocks_in_history={thinking_count}") |
| 77 | + # cache_control 存在检测 |
| 78 | + has_cc = False |
| 79 | + for section in ( |
| 80 | + body.get("system", []) if isinstance(body.get("system"), list) else [], |
| 81 | + *( |
| 82 | + m.get("content", []) |
| 83 | + for m in body.get("messages", []) |
| 84 | + if isinstance(m.get("content"), list) |
| 85 | + ), |
| 86 | + body.get("tools", []), |
| 87 | + ): |
| 88 | + if isinstance(section, list): |
| 89 | + for item in section: |
| 90 | + if isinstance(item, dict) and "cache_control" in item: |
| 91 | + has_cc = True |
| 92 | + break |
| 93 | + if has_cc: |
| 94 | + break |
| 95 | + if has_cc: |
| 96 | + parts.append("cache_control_fields=present") |
| 97 | + # 模型 + 消息数 |
| 98 | + parts.append(f"model={body.get('model', 'N/A')}") |
| 99 | + parts.append(f"messages={len(body.get('messages', []))}") |
| 100 | + return f" [{', '.join(parts)}]" if parts else "" |
| 101 | + |
| 102 | + |
51 | 103 | def _log_http_error_detail( |
52 | 104 | tier_name: str, |
53 | 105 | exc: Exception, |
@@ -601,12 +653,14 @@ async def execute_message( |
601 | 653 | ) |
602 | 654 |
|
603 | 655 | if not is_last and is_semantic: |
| 656 | + diagnostic = _build_semantic_rejection_diagnostic(body) |
604 | 657 | logger.warning( |
605 | | - "Tier %s semantic rejection (type=%s, msg=%s), " |
| 658 | + "Tier %s semantic rejection (type=%s, msg=%s)%s, " |
606 | 659 | "trying next tier without recording failure", |
607 | 660 | tier.name, |
608 | 661 | resp.error_type or resp.status_code, |
609 | 662 | (resp.error_message or "N/A")[:200], |
| 663 | + diagnostic, |
610 | 664 | ) |
611 | 665 | failed_tier_name = tier.name |
612 | 666 | continue |
@@ -838,6 +892,18 @@ async def _handle_http_error( |
838 | 892 | ) |
839 | 893 |
|
840 | 894 | if semantic_rejection and not is_last: |
| 895 | + if request_body is not None: |
| 896 | + diagnostic = _build_semantic_rejection_diagnostic(request_body) |
| 897 | + logger.warning( |
| 898 | + "Tier %s stream semantic rejection (type=%s, msg=%s)%s, " |
| 899 | + "trying next tier without recording failure", |
| 900 | + tier.name, |
| 901 | + error.get("type") if isinstance(error, dict) else None, |
| 902 | + (error.get("message") if isinstance(error, dict) else "N/A")[ |
| 903 | + :200 |
| 904 | + ], |
| 905 | + diagnostic, |
| 906 | + ) |
841 | 907 | return True, tier.name, exc |
842 | 908 |
|
843 | 909 | rl_info = parse_rate_limit_headers( |
|
0 commit comments