fix: Antigravity Claude tool ID 匹配与 thinking budget 输出约束

- 重构 _inject_claude_tool_ids_request 采用两遍扫描算法:
  * 第一遍收集所有 functionCall 的 ID(按 name 分组)
  * 第二遍为 functionResponse 从对应队列中取出匹配的 ID
  * 解决多工具调用场景下 ID 匹配错乱问题

- 增强 _process_thinking_budget 确保 maxOutputTokens > thinkingBudget:
  * 新增 OUTPUT_OVERHEAD/OUTPUT_OVERHEAD_IMAGE 常量(对齐 AM)
  * 自动调整 maxOutputTokens 为 budget + overhead
  * 超过 MODEL_MAX_OUTPUT_LIMIT 时减少 budget 而非超限
This commit is contained in:
AAEE86
2026-02-10 05:12:21 +08:00
parent 5d36fd7f99
commit d4fd3518bf
2 changed files with 129 additions and 22 deletions

View File

@@ -74,6 +74,11 @@ MIN_SIGNATURE_LENGTH = 50 # 与 Antigravity-Manager 对齐
# ============== Thinking Budget ==============
THINKING_BUDGET_AUTO_CAP = 24576
THINKING_BUDGET_DEFAULT_INJECT = 24576 # 对齐 AM wrapper.rs (was 16000)
# 给输出留的空间(对齐 Antigravity-Manager普通模型 8192图像模型 2048
OUTPUT_OVERHEAD = 8192
OUTPUT_OVERHEAD_IMAGE = 2048
# 模型最大输出限制(防止超限)
MODEL_MAX_OUTPUT_LIMIT = 65536
# 包含这些关键字的模型会自动注入 thinkingConfig如果缺失
THINKING_MODELS_AUTO_INJECT_KEYWORDS = ("thinking", "gemini-2.0-pro", "gemini-3-pro")
@@ -191,7 +196,10 @@ __all__ = [
"IMAGE_GEN_UPSTREAM_MODEL",
"MIN_SIGNATURE_LENGTH",
"MODEL_ALIAS_MAP",
"MODEL_MAX_OUTPUT_LIMIT",
"NETWORKING_TOOL_KEYWORDS",
"OUTPUT_OVERHEAD",
"OUTPUT_OVERHEAD_IMAGE",
"PROD_BASE_URL",
"REQUEST_USER_AGENT",
"RETRY_429_BASE_SECONDS",

View File

@@ -30,7 +30,10 @@ from src.services.provider.adapters.antigravity.constants import (
IMAGE_ASPECT_RATIO_SUFFIXES,
IMAGE_GEN_UPSTREAM_MODEL,
MODEL_ALIAS_MAP,
MODEL_MAX_OUTPUT_LIMIT,
NETWORKING_TOOL_KEYWORDS,
OUTPUT_OVERHEAD,
OUTPUT_OVERHEAD_IMAGE,
)
from src.services.provider.adapters.antigravity.constants import (
REQUEST_USER_AGENT as ANTIGRAVITY_REQUEST_USER_AGENT,
@@ -139,6 +142,10 @@ def _inject_claude_tool_ids_request(inner_request: dict[str, Any], model: str) -
Google v1internal 在目标模型为 Claude 时要求 functionCall 带有 id 字段,
但标准 Gemini 协议不包含此字段。对齐 AM wrapper.rs #1522。
算法:
1. 第一遍:扫描所有 functionCall为没有 id 的生成 id并记录 (name, id) 队列
2. 第二遍:扫描所有 functionResponse从对应 name 的队列中取出 id 使用
"""
if "claude" not in model.lower():
return
@@ -147,6 +154,14 @@ def _inject_claude_tool_ids_request(inner_request: dict[str, Any], model: str) -
if not isinstance(contents, list):
return
# 第一遍:收集所有 functionCall 的 ID按 name 分组,保持顺序)
# name -> [id1, id2, ...] 每个调用的 ID 按出现顺序排列
call_ids_by_name: dict[str, list[str]] = {}
# 所有 call ID 按原始出现顺序(用于 fallback 匹配)
all_call_ids_ordered: list[str] = []
# 用于生成新 ID 的计数器(全局,确保唯一性)
name_counters: dict[str, int] = {}
for content in contents:
if not isinstance(content, dict):
continue
@@ -154,41 +169,98 @@ def _inject_claude_tool_ids_request(inner_request: dict[str, Any], model: str) -
if not isinstance(parts, list):
continue
# 每条消息维护独立的计数器(确保 Call 和 Response 生成匹配的 ID
name_counters: dict[str, int] = {}
for part in parts:
if not isinstance(part, dict):
continue
fc = part.get("functionCall") or part.get("function_call")
if isinstance(fc, dict):
name = fc.get("name", "")
if not isinstance(name, str) or not name:
name = "unknown"
fc_id = fc.get("id")
if fc_id is None:
# 生成新 ID
count = name_counters.get(name, 0)
fc_id = f"call_{name}_{count}"
fc["id"] = fc_id
name_counters[name] = count + 1
# 记录这个 call 的 ID供后续 response 使用
if name not in call_ids_by_name:
call_ids_by_name[name] = []
call_ids_by_name[name].append(fc_id)
# 同时按原始出现顺序记录(用于 fallback
all_call_ids_ordered.append(fc_id)
# 第二遍:为 functionResponse 分配匹配的 ID
# 使用索引追踪每个 name 已消费到第几个 ID
response_index_by_name: dict[str, int] = {}
# 已使用的 call ID 集合
used_call_ids: set[str] = set()
# fallback 用的有序索引
fallback_index = 0
for content in contents:
if not isinstance(content, dict):
continue
parts = content.get("parts")
if not isinstance(parts, list):
continue
for part in parts:
if not isinstance(part, dict):
continue
# 1. functionCallAssistant 请求调用工具)- 支持 camelCase 和 snake_case
fc = part.get("functionCall") or part.get("function_call")
if isinstance(fc, dict) and fc.get("id") is None:
name = fc.get("name", "unknown")
if not isinstance(name, str):
name = "unknown"
count = name_counters.get(name, 0)
fc["id"] = f"call_{name}_{count}"
name_counters[name] = count + 1
# 2. functionResponseUser 回复工具结果)- 支持 camelCase 和 snake_case
fr = part.get("functionResponse") or part.get("function_response")
if isinstance(fr, dict) and fr.get("id") is None:
name = fr.get("name", "unknown")
if not isinstance(name, str):
name = fr.get("name", "")
if not isinstance(name, str) or not name:
name = "unknown"
count = name_counters.get(name, 0)
fr["id"] = f"call_{name}_{count}"
name_counters[name] = count + 1
assigned_id: str | None = None
# 优先:从对应 name 的 call ID 队列中取出下一个 ID
call_ids = call_ids_by_name.get(name, [])
idx = response_index_by_name.get(name, 0)
while idx < len(call_ids):
cid = call_ids[idx]
idx += 1
if cid not in used_call_ids:
assigned_id = cid
used_call_ids.add(cid)
break
response_index_by_name[name] = idx
# Fallback如果 name 是 unknown原本为空尝试使用下一个未使用的 call ID按原始出现顺序
if assigned_id is None and name == "unknown":
while fallback_index < len(all_call_ids_ordered):
cid = all_call_ids_ordered[fallback_index]
fallback_index += 1
if cid not in used_call_ids:
assigned_id = cid
used_call_ids.add(cid)
break
if assigned_id is not None:
fr["id"] = assigned_id
else:
# 没有匹配的 call ID异常情况生成一个新 ID
count = name_counters.get(name, 0)
fr["id"] = f"call_{name}_{count}"
name_counters[name] = count + 1
def _process_thinking_budget(inner_request: dict[str, Any], model: str) -> None:
"""处理 Thinking Budget自动注入 + Auto Cap。
"""处理 Thinking Budget自动注入 + Auto Cap + maxOutputTokens 约束
对齐 AM wrapper.rs
对齐 AM wrapper.rs + CLIProxyAPI 混合方案
- 对 flash/pro/thinking 模型处理 thinkingConfig
- 自动注入 thinkingConfig对已知需要 thinking 的模型)
- Auto Capbudget 超过 24576 时裁剪
- Claude 要求maxOutputTokens 必须 > thinkingBudget
- 优先增加 maxOutputTokens超限时才减少 budget
"""
lower_model = model.lower()
if not any(kw in lower_model for kw in ("flash", "pro", "thinking")):
@@ -214,8 +286,35 @@ def _process_thinking_budget(inner_request: dict[str, Any], model: str) -> None:
return
budget = thinking_config.get("thinkingBudget")
if isinstance(budget, int) and budget > THINKING_BUDGET_AUTO_CAP:
thinking_config["thinkingBudget"] = THINKING_BUDGET_AUTO_CAP
if not isinstance(budget, int):
return
# 1. 限制 budget 上限
if budget > THINKING_BUDGET_AUTO_CAP:
budget = THINKING_BUDGET_AUTO_CAP
thinking_config["thinkingBudget"] = budget
# 2. 确保 maxOutputTokens > thinkingBudget
current_max = gen_config.get("maxOutputTokens")
# 根据模型类型选择增量(对齐 Antigravity-Manager
overhead = OUTPUT_OVERHEAD_IMAGE if "-image" in lower_model else OUTPUT_OVERHEAD
# 计算理想的 maxOutputTokens
ideal_max = budget + overhead
# 3. 确保不超过模型限制
if ideal_max > MODEL_MAX_OUTPUT_LIMIT:
# 超限时:减少 budget 而不是超限(对齐 CLIProxyAPI 策略)
ideal_max = MODEL_MAX_OUTPUT_LIMIT
# 确保 budget < ideal_max保留 overhead 空间给输出
max_budget = ideal_max - overhead
if budget > max_budget:
thinking_config["thinkingBudget"] = max_budget
# 4. 应用修正
if current_max is None or (isinstance(current_max, int) and current_max <= budget):
gen_config["maxOutputTokens"] = ideal_max
def _clean_tool_declarations(inner_request: dict[str, Any]) -> None: