fix(llm): isolate invalid output failures and trace argument streams
This commit is contained in:
1 parent
b53ca18595
commit
486e6d550d
10 files changed
+356
-24
No files matched your search
@@ -0,0 +1,53 @@
|
||||
"""Bounded fingerprints of received argument deltas, without retaining text."""
|
||||
|
||||
import hashlib
|
||||
|
||||
|
||||
PREFIX_BYTES = 4096
|
||||
MAX_PREFIXES = 16
|
||||
MAX_TRACKED_TOOLS = 64
|
||||
|
||||
|
||||
class ArgumentFingerprint:
|
||||
def __init__(self):
|
||||
self.characters = 0
|
||||
self.bytes = 0
|
||||
self.deltas = 0
|
||||
self.encoding_valid = True
|
||||
self.digest = hashlib.sha256()
|
||||
self.prefixes: list[dict] = []
|
||||
|
||||
def observe(self, arguments: str) -> None:
|
||||
self.characters += len(arguments)
|
||||
if not arguments:
|
||||
return
|
||||
self.deltas += 1
|
||||
if not self.encoding_valid:
|
||||
return
|
||||
try:
|
||||
encoded = arguments.encode("utf-8")
|
||||
except UnicodeEncodeError:
|
||||
# Diagnostics must not repair, reject or otherwise alter the stream.
|
||||
self.encoding_valid = False
|
||||
return
|
||||
offset = 0
|
||||
while offset < len(encoded) and len(self.prefixes) < MAX_PREFIXES:
|
||||
size = min(PREFIX_BYTES - self.bytes % PREFIX_BYTES, len(encoded) - offset)
|
||||
self.digest.update(encoded[offset : offset + size])
|
||||
self.bytes += size
|
||||
offset += size
|
||||
if self.bytes % PREFIX_BYTES == 0:
|
||||
self.prefixes.append({"bytes": self.bytes, "sha256": self.digest.hexdigest()})
|
||||
if offset < len(encoded):
|
||||
self.digest.update(encoded[offset:])
|
||||
self.bytes += len(encoded) - offset
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
return {
|
||||
"argument_chars": self.characters,
|
||||
"argument_bytes": self.bytes if self.encoding_valid else None,
|
||||
"argument_sha256": self.digest.hexdigest() if self.encoding_valid else None,
|
||||
"argument_deltas": self.deltas,
|
||||
"argument_prefixes": [dict(prefix) for prefix in self.prefixes],
|
||||
"argument_encoding_valid": self.encoding_valid,
|
||||
}
|
||||
+35
-10
@@ -13,6 +13,7 @@ from uuid import uuid4
|
||||
from PIL import Image
|
||||
|
||||
from artemis.data_engine.context_vars import CURRENT_NODE_NAME, CURRENT_TRACE_ID
|
||||
from artemis.llm.argument_fingerprint import ArgumentFingerprint, MAX_TRACKED_TOOLS
|
||||
from artemis.llm.final_review import ACTIVE_FINAL_REVIEW, FinalReviewBudgetExceeded
|
||||
from artemis.llm.reliability import LLMExhaustedError
|
||||
from artemis.llm.usage import usage_from_message
|
||||
@@ -107,6 +108,7 @@ class RequestAttempt:
|
||||
self.clock = time.monotonic()
|
||||
self.last_publish = self.clock
|
||||
self.tools: dict[int | str, dict] = {}
|
||||
self.tool_fingerprints: dict[int | str, ArgumentFingerprint] = {}
|
||||
self.review = ACTIVE_FINAL_REVIEW.get()
|
||||
if self.review:
|
||||
self.review.request_started(request_id, attempt)
|
||||
@@ -126,6 +128,9 @@ class RequestAttempt:
|
||||
"usage": None,
|
||||
"output_chunks": 0,
|
||||
"tool_argument_chars": 0,
|
||||
"tool_observation_layer": "langchain_tool_call_chunks",
|
||||
"argument_fingerprint_version": 1,
|
||||
"tool_stats_truncated": False,
|
||||
"tools": [],
|
||||
"step_id": str(self.step_id) if self.step_id else None,
|
||||
"parent_trace_id": str(self.parent_id) if self.parent_id else None,
|
||||
@@ -167,9 +172,10 @@ class RequestAttempt:
|
||||
self.data.update(
|
||||
headers_at=time.time(),
|
||||
http_status=response.status_code,
|
||||
server_request_id=_identifier(
|
||||
response.headers.get("x-request-id") or response.headers.get("request-id")
|
||||
),
|
||||
server_request_id=_identifier(response.headers.get("x-oneapi-request-id"))
|
||||
or _identifier(response.headers.get("x-request-id"))
|
||||
or _identifier(response.headers.get("request-id")),
|
||||
response_x_request_id=_identifier(response.headers.get("x-request-id")),
|
||||
server_trace_id=_identifier(response.headers.get("x-trace-id")),
|
||||
phase="waiting_output",
|
||||
)
|
||||
@@ -196,18 +202,31 @@ class RequestAttempt:
|
||||
self.data["usage"] = usage
|
||||
tool_chunks = getattr(chunk, "tool_call_chunks", None) or []
|
||||
# Responses exposes the same argument delta in content and tool_call_chunks.
|
||||
# Count only the latter; retain lengths/names, never argument bodies.
|
||||
# Count only the latter; retain bounded fingerprints, never argument bodies.
|
||||
for tool in tool_chunks:
|
||||
arguments = tool.get("args")
|
||||
if isinstance(arguments, str):
|
||||
self.data["tool_argument_chars"] += len(arguments)
|
||||
index = tool.get("index")
|
||||
key = index if index is not None else tool.get("id") or "unknown"
|
||||
stats = self.tools.setdefault(key, {"name": None, "argument_chars": 0})
|
||||
call_id = _identifier(tool.get("id"))
|
||||
key = index if index is not None else call_id or "unknown"
|
||||
if key not in self.tools and len(self.tools) >= MAX_TRACKED_TOOLS:
|
||||
self.data["tool_stats_truncated"] = True
|
||||
continue
|
||||
stats = self.tools.setdefault(key, {"name": None})
|
||||
if call_id:
|
||||
stats["call_id"] = call_id
|
||||
if index is not None:
|
||||
stats["chunk_index"] = index
|
||||
name = tool.get("name")
|
||||
if isinstance(name, str) and re.fullmatch(r"[\w.:-]{1,128}", name):
|
||||
stats["name"] = name
|
||||
arguments = tool.get("args")
|
||||
fingerprint = self.tool_fingerprints.get(key)
|
||||
if fingerprint is None:
|
||||
fingerprint = self.tool_fingerprints[key] = ArgumentFingerprint()
|
||||
if isinstance(arguments, str):
|
||||
stats["argument_chars"] += len(arguments)
|
||||
self.data["tool_argument_chars"] += len(arguments)
|
||||
fingerprint.observe(arguments)
|
||||
stats.update(fingerprint.snapshot())
|
||||
self.data["tools"] = list(self.tools.values())
|
||||
content = getattr(chunk, "content", None)
|
||||
substantive = isinstance(content, str) and bool(content)
|
||||
@@ -252,7 +271,9 @@ class RequestAttempt:
|
||||
if isinstance(status, int):
|
||||
self.data["http_status"] = status
|
||||
if request_id := _identifier(getattr(error, "request_id", None)):
|
||||
self.data["server_request_id"] = request_id
|
||||
self.data["error_request_id"] = request_id
|
||||
if not self.data.get("server_request_id"):
|
||||
self.data["server_request_id"] = request_id
|
||||
if category == "timeout":
|
||||
self.data.setdefault("timeout_scope", "transport")
|
||||
if response is not None:
|
||||
@@ -301,6 +322,7 @@ async def observe_request_headers(request):
|
||||
if attempt is None:
|
||||
return
|
||||
request.headers.setdefault("X-Client-Request-Id", attempt.data["request_id"])
|
||||
request.headers.setdefault("X-Client-Attempt-Id", attempt.data["attempt_id"])
|
||||
routing = {}
|
||||
for name in ("thread-id", "thread_id", "session-id", "session_id"):
|
||||
value = request.headers.get(name)
|
||||
@@ -309,9 +331,12 @@ async def observe_request_headers(request):
|
||||
attempt.data.update(
|
||||
request_sent_at=time.time(),
|
||||
client_request_id=_identifier(request.headers.get("X-Client-Request-Id")),
|
||||
client_attempt_id=_identifier(request.headers.get("X-Client-Attempt-Id")),
|
||||
routing_header_fingerprints=routing,
|
||||
client_request_id_matches=request.headers.get("X-Client-Request-Id")
|
||||
== attempt.data["request_id"],
|
||||
client_attempt_id_matches=request.headers.get("X-Client-Attempt-Id")
|
||||
== attempt.data["attempt_id"],
|
||||
)
|
||||
attempt.publish()
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class LLMPermanentError(LLMCallError):
|
||||
|
||||
|
||||
class LLMExhaustedError(LLMCallError):
|
||||
"""Retryable failures exhausted every attempt and the pause deadline."""
|
||||
"""Retryable failures exhausted attempts and any applicable pause deadline."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -23,7 +23,7 @@ class ToolArgumentOverflow(ValueError):
|
||||
"limit_chars": SMALL_ACTION_ARGUMENT_LIMIT,
|
||||
}
|
||||
super().__init__(
|
||||
f"模型生成的 {name} 参数异常膨胀({characters} 字符,"
|
||||
f"收到的 {name} 参数流超出安全限制({characters} 字符,"
|
||||
f"保护上限 {SMALL_ACTION_ARGUMENT_LIMIT});已关闭输出流,未执行本次动作。"
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user