mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: 动态 block 索引分配、OAuth 自动启停联动与 Antigravity 签名注入增强
- Gemini/OpenAI normalizer 改为按实际出现顺序延迟分配 thinking/text block 索引 - OAuth 失效标记时自动停用 Key,清除/刷新成功时自动启用 - Antigravity thought signature 注入支持 tool-specific 签名优先,再回退 session 级签名 - Claude normalizer 对非字符串 thinking block 降级为 UnknownBlock - Gemini normalizer 反序列化时为 Antigravity 目标补充 signature - signature_cache _prune 增加超限驱逐 - model/provider 包 __init__ 改为惰性导入避免循环依赖 - build_antigravity_url 复制 query_params 防止修改调用方原始字典 - upstream_fetcher build_all_format_configs 兼容非 EndpointFetchConfig 对象 - Antigravity HTTP 状态判定扩展与 Gemini endpoint check 支持 v1internal - 新增 thought signature 注入与 signature cache 驱逐测试 Closes #165 Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -720,15 +720,21 @@ class ClaudeNormalizer(FormatNormalizer):
|
||||
|
||||
btype = str(block.get("type") or "unknown")
|
||||
if btype == "thinking":
|
||||
thinking = str(block.get("thinking") or "")
|
||||
thinking_raw = block.get("thinking")
|
||||
signature = block.get("signature")
|
||||
if thinking:
|
||||
|
||||
if isinstance(thinking_raw, str) and thinking_raw:
|
||||
blocks.append(
|
||||
ThinkingBlock(
|
||||
thinking=thinking,
|
||||
thinking=thinking_raw,
|
||||
signature=str(signature) if signature else None,
|
||||
)
|
||||
)
|
||||
else:
|
||||
dropped_key = f"claude_block:{btype}"
|
||||
dropped[dropped_key] = dropped.get(dropped_key, 0) + 1
|
||||
blocks.append(UnknownBlock(raw_type=btype, payload=block))
|
||||
|
||||
continue
|
||||
|
||||
if btype == "text":
|
||||
|
||||
@@ -565,12 +565,27 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
state.message_id = state.message_id or "gemini"
|
||||
ss["message_started"] = True
|
||||
ss.setdefault("text_block_started", False)
|
||||
ss.setdefault("text_block_stopped", False)
|
||||
ss.setdefault("thinking_block_started", False)
|
||||
ss.setdefault("thinking_block_stopped", False)
|
||||
# 根据首先出现的内容类型延迟分配块索引。
|
||||
# 当不存在思考内容时,golden tests 期望文本位于索引0。
|
||||
ss.setdefault("text_block_index", None)
|
||||
ss.setdefault("thinking_block_index", None)
|
||||
ss.setdefault("accumulated_text", "")
|
||||
ss.setdefault("accumulated_thinking", "")
|
||||
ss.setdefault("next_block_index", 2) # 0 预留给 thinking, 1 预留给 text
|
||||
ss.setdefault("next_block_index", 0)
|
||||
events.append(MessageStartEvent(message_id=state.message_id, model=model))
|
||||
|
||||
def _reserve_block_index(slot_key: str) -> int:
|
||||
idx = ss.get(slot_key)
|
||||
if isinstance(idx, int) and idx >= 0:
|
||||
return idx
|
||||
next_idx = int(ss.get("next_block_index") or 0)
|
||||
ss[slot_key] = next_idx
|
||||
ss["next_block_index"] = next_idx + 1
|
||||
return next_idx
|
||||
|
||||
candidates = chunk.get("candidates") or []
|
||||
if not isinstance(candidates, list) or not candidates:
|
||||
return events
|
||||
@@ -595,7 +610,7 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
is_thought = part.get("thought") is True
|
||||
|
||||
if is_thought:
|
||||
# Thinking content → block_index=0
|
||||
# Thinking content
|
||||
prev = str(ss.get("accumulated_thinking") or "")
|
||||
if text.startswith(prev):
|
||||
delta = text[len(prev) :]
|
||||
@@ -609,12 +624,18 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
ss["thinking_block_started"] = True
|
||||
events.append(
|
||||
ContentBlockStartEvent(
|
||||
block_index=0, block_type=ContentType.THINKING
|
||||
block_index=_reserve_block_index("thinking_block_index"),
|
||||
block_type=ContentType.THINKING,
|
||||
)
|
||||
)
|
||||
events.append(ContentDeltaEvent(block_index=0, text_delta=delta))
|
||||
events.append(
|
||||
ContentDeltaEvent(
|
||||
block_index=_reserve_block_index("thinking_block_index"),
|
||||
text_delta=delta,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Regular text → block_index=1
|
||||
# Regular text
|
||||
prev = str(ss.get("accumulated_text") or "")
|
||||
if text.startswith(prev):
|
||||
delta = text[len(prev) :]
|
||||
@@ -624,29 +645,38 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
ss["accumulated_text"] = prev + delta
|
||||
|
||||
if delta:
|
||||
# 切换:先关闭 thinking block(Claude 协议要求顺序 stop/start)
|
||||
# Transition: stop thinking block before text (Claude requires stop/start ordering).
|
||||
if ss.get("thinking_block_started") and not ss.get(
|
||||
"thinking_block_stopped"
|
||||
):
|
||||
ss["thinking_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=0))
|
||||
events.append(
|
||||
ContentBlockStopEvent(
|
||||
block_index=_reserve_block_index("thinking_block_index")
|
||||
)
|
||||
)
|
||||
|
||||
if not ss.get("text_block_started"):
|
||||
ss["text_block_started"] = True
|
||||
events.append(
|
||||
ContentBlockStartEvent(
|
||||
block_index=1, block_type=ContentType.TEXT
|
||||
block_index=_reserve_block_index("text_block_index"),
|
||||
block_type=ContentType.TEXT,
|
||||
)
|
||||
)
|
||||
events.append(ContentDeltaEvent(block_index=1, text_delta=delta))
|
||||
events.append(
|
||||
ContentDeltaEvent(
|
||||
block_index=_reserve_block_index("text_block_index"),
|
||||
text_delta=delta,
|
||||
)
|
||||
)
|
||||
|
||||
# 提取 thoughtSignature(对齐 AM:缓存到 session)
|
||||
sig = part.get("thoughtSignature") or part.get("thought_signature")
|
||||
if isinstance(sig, str) and sig and ss.get("thinking_block_started"):
|
||||
# 仅在 thinking block 已开启时发射 signature delta
|
||||
events.append(
|
||||
ContentDeltaEvent(
|
||||
block_index=0,
|
||||
block_index=_reserve_block_index("thinking_block_index"),
|
||||
text_delta="",
|
||||
extra={"thought_signature": sig},
|
||||
)
|
||||
@@ -662,10 +692,10 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
# 关闭前面的 thinking/text block(如果还开着)
|
||||
if ss.get("thinking_block_started") and not ss.get("thinking_block_stopped"):
|
||||
ss["thinking_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=0))
|
||||
events.append(ContentBlockStopEvent(block_index=_reserve_block_index("thinking_block_index")))
|
||||
if ss.get("text_block_started") and not ss.get("text_block_stopped"):
|
||||
ss["text_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=1))
|
||||
events.append(ContentBlockStopEvent(block_index=_reserve_block_index("text_block_index")))
|
||||
|
||||
name = str(func_call.get("name") or "")
|
||||
args = func_call.get("args")
|
||||
@@ -676,7 +706,7 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
fc_id = func_call.get("id")
|
||||
tool_id = fc_id if isinstance(fc_id, str) and fc_id else None
|
||||
|
||||
block_index = int(ss.get("next_block_index") or 2)
|
||||
block_index = int(ss.get("next_block_index") or 0)
|
||||
ss["next_block_index"] = block_index + 1
|
||||
|
||||
events.append(
|
||||
@@ -711,7 +741,7 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
|
||||
# 确保 mime_type 和 data 都非空
|
||||
if mime_type and data and len(data) > 10: # base64 图片数据至少几十个字符
|
||||
block_index = int(ss.get("next_block_index") or 1)
|
||||
block_index = int(ss.get("next_block_index") or 0)
|
||||
ss["next_block_index"] = block_index + 1
|
||||
|
||||
# 使用 ContentBlockStartEvent 传递图片数据
|
||||
@@ -736,10 +766,10 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
# 先补齐 content_block_stop(所有已开启的 block),再发送 MessageStop
|
||||
if ss.get("thinking_block_started") and not ss.get("thinking_block_stopped"):
|
||||
ss["thinking_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=0))
|
||||
events.append(ContentBlockStopEvent(block_index=_reserve_block_index("thinking_block_index")))
|
||||
if ss.get("text_block_started") and not ss.get("text_block_stopped"):
|
||||
ss["text_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=1))
|
||||
events.append(ContentBlockStopEvent(block_index=_reserve_block_index("text_block_index")))
|
||||
events.append(MessageStopEvent(stop_reason=stop_reason, usage=usage_info))
|
||||
|
||||
if "error" in chunk:
|
||||
@@ -1377,9 +1407,41 @@ class GeminiNormalizer(FormatNormalizer):
|
||||
|
||||
if isinstance(b, ThinkingBlock):
|
||||
if b.thinking:
|
||||
signature: str | None = b.signature or None
|
||||
|
||||
if signature is None and target_variant == "antigravity":
|
||||
model_str = str(model or "")
|
||||
try:
|
||||
from src.services.provider.adapters.antigravity.constants import (
|
||||
DUMMY_THOUGHT_SIGNATURE,
|
||||
)
|
||||
from src.services.provider.adapters.antigravity.signature_cache import (
|
||||
signature_cache,
|
||||
)
|
||||
|
||||
cached_or_dummy = signature_cache.get_or_dummy(model_str, b.thinking)
|
||||
|
||||
# Prefer cached real signature > dummy signature.
|
||||
if (
|
||||
isinstance(cached_or_dummy, str)
|
||||
and cached_or_dummy
|
||||
and cached_or_dummy != DUMMY_THOUGHT_SIGNATURE
|
||||
):
|
||||
signature = cached_or_dummy
|
||||
elif isinstance(cached_or_dummy, str) and cached_or_dummy:
|
||||
signature = cached_or_dummy
|
||||
except Exception:
|
||||
# Best-effort fallback: Gemini models can accept a dummy signature.
|
||||
if model_str.startswith("gemini-"):
|
||||
signature = "skip_thought_signature_validator"
|
||||
|
||||
# For Antigravity, missing signature is likely to fail upstream validation.
|
||||
if target_variant == "antigravity" and not signature:
|
||||
continue
|
||||
|
||||
thought_part: dict[str, Any] = {"text": b.thinking, "thought": True}
|
||||
if b.signature:
|
||||
thought_part["thoughtSignature"] = b.signature
|
||||
if signature:
|
||||
thought_part["thoughtSignature"] = signature
|
||||
parts.append(thought_part)
|
||||
continue
|
||||
|
||||
|
||||
@@ -402,11 +402,26 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
state.model = model
|
||||
ss["message_started"] = True
|
||||
ss.setdefault("thinking_block_started", False)
|
||||
ss.setdefault("thinking_block_stopped", False)
|
||||
ss.setdefault("text_block_started", False)
|
||||
ss.setdefault("text_block_stopped", False)
|
||||
# 根据首先出现的内容类型延迟分配块索引。
|
||||
# 这样可以避免预留空白:当不存在思考内容时,测试期望文本位于索引0。
|
||||
ss.setdefault("thinking_block_index", None)
|
||||
ss.setdefault("text_block_index", None)
|
||||
ss.setdefault("tool_id_to_block_index", {})
|
||||
ss.setdefault("next_block_index", 2) # 0=thinking, 1=text, 2+=tools
|
||||
ss.setdefault("next_block_index", 0)
|
||||
events.append(MessageStartEvent(message_id=msg_id, model=model))
|
||||
|
||||
def _reserve_block_index(slot_key: str) -> int:
|
||||
idx = ss.get(slot_key)
|
||||
if isinstance(idx, int) and idx >= 0:
|
||||
return idx
|
||||
next_idx = int(ss.get("next_block_index") or 0)
|
||||
ss[slot_key] = next_idx
|
||||
ss["next_block_index"] = next_idx + 1
|
||||
return next_idx
|
||||
|
||||
choices = chunk.get("choices") or []
|
||||
if not choices or not isinstance(choices, list):
|
||||
# OpenAI streaming may send a final "usage-only" chunk when
|
||||
@@ -432,24 +447,44 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
# reasoning_content delta (thinking)
|
||||
reasoning_delta = delta.get("reasoning_content")
|
||||
if isinstance(reasoning_delta, str) and reasoning_delta:
|
||||
thinking_index = _reserve_block_index("thinking_block_index")
|
||||
if not ss.get("thinking_block_started"):
|
||||
ss["thinking_block_started"] = True
|
||||
events.append(
|
||||
ContentBlockStartEvent(block_index=0, block_type=ContentType.THINKING)
|
||||
ContentBlockStartEvent(
|
||||
block_index=thinking_index,
|
||||
block_type=ContentType.THINKING,
|
||||
)
|
||||
)
|
||||
events.append(ContentDeltaEvent(block_index=0, text_delta=reasoning_delta))
|
||||
events.append(ContentDeltaEvent(block_index=thinking_index, text_delta=reasoning_delta))
|
||||
|
||||
# content delta
|
||||
content_delta = delta.get("content")
|
||||
if isinstance(content_delta, str) and content_delta:
|
||||
# 从 thinking 过渡到 text 时,先关闭 thinking block
|
||||
if ss.get("thinking_block_started") and not ss.get("thinking_block_stopped"):
|
||||
# From thinking -> text, close thinking block first (only when text hasn't started yet).
|
||||
if (
|
||||
ss.get("thinking_block_started")
|
||||
and not ss.get("thinking_block_stopped")
|
||||
and not ss.get("text_block_started")
|
||||
):
|
||||
ss["thinking_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=0))
|
||||
events.append(
|
||||
ContentBlockStopEvent(block_index=_reserve_block_index("thinking_block_index"))
|
||||
)
|
||||
if not ss.get("text_block_started"):
|
||||
ss["text_block_started"] = True
|
||||
events.append(ContentBlockStartEvent(block_index=1, block_type=ContentType.TEXT))
|
||||
events.append(ContentDeltaEvent(block_index=1, text_delta=content_delta))
|
||||
events.append(
|
||||
ContentBlockStartEvent(
|
||||
block_index=_reserve_block_index("text_block_index"),
|
||||
block_type=ContentType.TEXT,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ContentDeltaEvent(
|
||||
block_index=_reserve_block_index("text_block_index"),
|
||||
text_delta=content_delta,
|
||||
)
|
||||
)
|
||||
|
||||
# tool_calls delta
|
||||
tool_calls = delta.get("tool_calls")
|
||||
@@ -497,10 +532,14 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
# 先补齐 content_block_stop(thinking + text),再发送 MessageStop
|
||||
if ss.get("thinking_block_started") and not ss.get("thinking_block_stopped"):
|
||||
ss["thinking_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=0))
|
||||
events.append(
|
||||
ContentBlockStopEvent(block_index=_reserve_block_index("thinking_block_index"))
|
||||
)
|
||||
if ss.get("text_block_started") and not ss.get("text_block_stopped"):
|
||||
ss["text_block_stopped"] = True
|
||||
events.append(ContentBlockStopEvent(block_index=1))
|
||||
events.append(
|
||||
ContentBlockStopEvent(block_index=_reserve_block_index("text_block_index"))
|
||||
)
|
||||
# 解析 usage(需要请求时设置 stream_options.include_usage: true)
|
||||
usage_info = self._openai_usage_to_internal(chunk.get("usage"))
|
||||
events.append(MessageStopEvent(stop_reason=stop_reason, usage=usage_info))
|
||||
@@ -1504,7 +1543,7 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
if tool_key in mapping:
|
||||
return int(mapping[tool_key])
|
||||
|
||||
next_idx = int(ss.get("next_block_index") or 1)
|
||||
next_idx = int(ss.get("next_block_index") or 0)
|
||||
mapping[tool_key] = next_idx
|
||||
ss["next_block_index"] = next_idx + 1
|
||||
return next_idx
|
||||
|
||||
Reference in New Issue
Block a user