AID-D-002.004: Reasoning Trace & Thinking-Token Anomaly Telemetry
Pillars: app, model, infra
Lifecycle: operation
Scope boundary
Owns anomalies in provider or API-visible reasoning metadata and accounting, including thinking-token counts, reasoning-to-visible-output ratios, redaction flags, trace-summary shape, display mode, latency, and reasoning-budget decisions. It does not semantically judge the content of an emitted reasoning trace.
AID-D-002.003: AID-D-002.004 monitors output-side reasoning metadata and accounting; AID-D-002.003 monitors white-box attention, logits, and activation-like internal signals. Reasoning blocks exposed by an API are not equivalent to direct instrumentation of model internals.
AID-D-005.007: AID-D-002.004 identifies reasoning-specific trace and thinking-token anomalies; AID-D-005.007 detects broader request-volume, token, tool-use, parameter, latency, and cost spikes. Reasoning telemetry feeds the broader consumption detector but retains its own semantics and applicability.
Implementation guidance
- AID-D-002.004-G001: Collect reasoning-token and trace-shape telemetry at the gateway and alert on reasoning-specific anomalies such as budget overruns, high hidden-output ratios, repeated redaction, or unexpected trace disclosure.
AID-D-002.004-G001
Observation boundary and responsibilities
Run this evaluator at the inference gateway over the complete immutable response-event population for a signed measurement window. Select JSON paths by exact provider, model, and API version from a signed capability profile; never infer that an absent value is zero or false. This detector emits reasoning-specific findings only. Blocking and budget enforcement remain with AID-H-037.002, reasoning confidentiality with AID-H-037.001, and general cost/latency detection with AID-D-005.007. Latency is retained below only as a correlation join field and is never scored here.
Signed inputs
Sign reasoning-capabilities.json, reasoning-detection-policy.json, the sanitized JSONL export, and a separately produced completeness receipt. Each capability entry has exact provider, model, and api_version keys, an applicable Boolean, JSON Pointer paths, and required fields. The receipt binds the source checkpoint, window, digest, record count, and eligible count. The policy owns every numeric threshold.
Verify, normalize, evaluate, and sign
from __future__ import annotations
import math as _aidefend_math
import os as _aidefend_os
# Admission injects these required values from one signature-verified, versioned runtime profile.
def _aidefend_required(name):
value = _aidefend_os.environ.get(name)
if value is None or not value.strip():
raise RuntimeError(f"required runtime-profile field is absent: {name}")
return value.strip()
def _aidefend_positive_float(name):
try:
value = float(_aidefend_required(name))
except ValueError as error:
raise RuntimeError(f"runtime-profile field is not numeric: {name}") from error
if not _aidefend_math.isfinite(value) or value <= 0:
raise RuntimeError(f"runtime-profile field must be finite and positive: {name}")
return value
def _aidefend_positive_int(name):
try:
value = int(_aidefend_required(name))
except ValueError as error:
raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
if value < 1:
raise RuntimeError(f"runtime-profile field must be positive: {name}")
return value
def _aidefend_nonnegative_int(name):
try:
value = int(_aidefend_required(name))
except ValueError as error:
raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
if value < 0:
raise RuntimeError(f"runtime-profile field cannot be negative: {name}")
return value
def _aidefend_fraction(name):
value = _aidefend_positive_float(name)
if value > 1:
raise RuntimeError(f"runtime-profile field must be in (0, 1]: {name}")
return value
RUNTIME_PROFILE_VERSION = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_VERSION")
RUNTIME_PROFILE_SHA256 = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_SHA256").lower()
if (len(RUNTIME_PROFILE_SHA256) != 64
or set(RUNTIME_PROFILE_SHA256) - set("0123456789abcdef")):
raise RuntimeError("runtime-profile digest must be lowercase SHA-256")
REASONING_TELEMETRY_COMMAND_TIMEOUT_SECONDS = _aidefend_positive_float("D002004_REASONING_TELEMETRY_COMMAND_TIMEOUT_SECONDS")
# file: detection/evaluate_reasoning_telemetry.py
import hashlib, json, math, os, subprocess
from datetime import datetime, timezone
from pathlib import Path
import hmac
import re
import tempfile
from typing import Any
RECORDS = Path("artifacts/reasoning/provider-responses.jsonl")
RECEIPT = Path("artifacts/reasoning/collection-receipt.json")
CAPS = Path("policy/reasoning-capabilities.json")
POLICY = Path("policy/reasoning-detection-policy.json")
EVIDENCE = Path("artifacts/reasoning/AID-D-002.004-evidence.json")
def sha256_file(path: Path) -> str:
captured = _VERIFIED_INPUTS.get(Path(path))
if captured is not None:
return hashlib.sha256(captured).hexdigest()
digest = hashlib.sha256()
with Path(path).open("rb") as handle:
for chunk in iter(lambda: handle.read(1048576), b""):
digest.update(chunk)
return digest.hexdigest()
_VERIFIED_INPUTS: dict[Path, bytes] = {}
def _write_private_snapshot(path: Path, payload: bytes) -> None:
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, "wb") as stream:
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
os.chmod(path, 0o400)
def _strict_json(raw: bytes | str) -> object:
text = raw.decode("utf-8", errors="strict") if isinstance(raw, bytes) else raw
def unique_object(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate JSON key: " + key)
value[key] = item
return value
return json.loads(
text,
object_pairs_hook=unique_object,
parse_constant=lambda value: (_ for _ in ()).throw(
ValueError("non-finite JSON constant: " + value)
),
)
def verify(path: Path, key_env: str) -> None:
payload_path = Path(path)
signature_path = Path(str(payload_path) + ".sig")
payload_raw = payload_path.read_bytes()
signature_raw = signature_path.read_bytes()
key_reference = os.environ[key_env]
remote_key = bool(re.match(
r"^[A-Za-z][A-Za-z0-9+.-]*://", key_reference
)) or key_reference.startswith("pkcs11:")
key_raw = None if remote_key else (
key_reference.encode("utf-8")
if key_reference.startswith("-----BEGIN ")
else Path(key_reference).read_bytes()
)
with tempfile.TemporaryDirectory(prefix="aidefend-d002004-") as directory:
root = Path(directory)
os.chmod(root, 0o700)
payload_snapshot = root / "payload"
signature_snapshot = root / "signature"
_write_private_snapshot(payload_snapshot, payload_raw)
_write_private_snapshot(signature_snapshot, signature_raw)
captured = [(payload_snapshot, payload_raw), (signature_snapshot, signature_raw)]
key_argument = key_reference
if key_raw is not None:
key_snapshot = root / "verify-key.pub"
_write_private_snapshot(key_snapshot, key_raw)
captured.append((key_snapshot, key_raw))
key_argument = str(key_snapshot)
try:
subprocess.run(
[
"cosign", "verify-blob", "--key", key_argument,
"--bundle", str(signature_snapshot), str(payload_snapshot),
],
check=True, capture_output=True, text=True,
timeout=REASONING_TELEMETRY_COMMAND_TIMEOUT_SECONDS,
)
if any(
not hmac.compare_digest(candidate.read_bytes(), expected)
for candidate, expected in captured
):
raise RuntimeError("private signature-verification snapshot changed")
finally:
for candidate, _ in captured:
os.chmod(candidate, 0o600)
_VERIFIED_INPUTS[payload_path] = payload_raw
def _verified_bytes(path: Path) -> bytes:
try:
return _VERIFIED_INPUTS[Path(path)]
except KeyError as error:
raise RuntimeError("signed input was not captured and verified") from error
def _verified_text(path: Path) -> str:
return _verified_bytes(path).decode("utf-8", errors="strict")
def timestamp(value: Any) -> datetime:
if not isinstance(value, str):
raise ValueError("timestamp is absent")
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise ValueError("timestamp lacks timezone")
return parsed.astimezone(timezone.utc)
def pointer(document: Any, path: str) -> tuple[bool, Any]:
if not isinstance(path, str) or not path.startswith("/"):
raise ValueError("invalid JSON Pointer")
current = document
for raw in path[1:].split("/"):
token = raw.replace("~1", "/").replace("~0", "~")
if isinstance(current, dict) and token in current:
current = current[token]
elif isinstance(current, list) and token.isdigit() and int(token) < len(current):
current = current[int(token)]
else:
return False, None
return True, current
def number(value: Any, field: str) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{field} is not numeric")
result = float(value)
if not math.isfinite(result) or result < 0:
raise ValueError(f"{field} is outside its domain")
return result
def load() -> tuple[list[dict], dict, dict, dict]:
verify(RECORDS, "D002004_COLLECTOR_VERIFY_KEY")
verify(RECEIPT, "D002004_COMPLETENESS_VERIFY_KEY")
verify(CAPS, "D002004_CAPABILITY_VERIFY_KEY")
verify(POLICY, "D002004_POLICY_VERIFY_KEY")
receipt = _strict_json(_verified_bytes(RECEIPT))
caps = _strict_json(_verified_bytes(CAPS))
policy = _strict_json(_verified_bytes(POLICY))
receipt_fields = {"schema_version", "window_start", "window_end", "source_id",
"source_checkpoint", "record_count", "eligible_record_count",
"records_sha256"}
if (not isinstance(receipt, dict) or set(receipt) != receipt_fields
or receipt["schema_version"] != "aidefend.d002004-receipt.v1"
or receipt["records_sha256"] != sha256_file(RECORDS)
or isinstance(receipt["record_count"], bool)
or not isinstance(receipt["record_count"], int)
or isinstance(receipt["eligible_record_count"], bool)
or not isinstance(receipt["eligible_record_count"], int)
or min(receipt["record_count"], receipt["eligible_record_count"]) < 0):
raise ValueError("collection receipt is invalid or misbound")
policy_fields = {"schema_version", "policy_version", "max_hidden_visible_ratio",
"min_hidden_tokens_without_output", "max_summary_chars",
"max_redacted_blocks", "evidence_max_age_seconds"}
if not isinstance(policy, dict) or set(policy) != policy_fields or policy["schema_version"] != "aidefend.d002004-policy.v1":
raise ValueError("policy schema differs")
for field in policy_fields - {"schema_version", "policy_version"}:
number(policy[field], field)
if (not isinstance(caps, dict)
or set(caps) != {"schema_version", "profile_version", "profiles"}
or caps["schema_version"] != "aidefend.reasoning-capabilities.v1"
or not isinstance(caps["profiles"], list)):
raise ValueError("capability profile schema differs")
lines = _verified_text(RECORDS).splitlines()
if any(not line.strip() for line in lines):
raise ValueError("JSONL contains a blank record")
records = [_strict_json(line) for line in lines]
if len(records) != receipt["record_count"]:
raise ValueError("record population differs")
ids = [record.get("record_id") for record in records if isinstance(record, dict)]
if len(ids) != len(records) or any(not isinstance(item, str) or not item for item in ids) or len(set(ids)) != len(ids):
raise ValueError("record IDs are absent or duplicated")
start, end = timestamp(receipt["window_start"]), timestamp(receipt["window_end"])
if start >= end:
raise ValueError("invalid measurement window")
fields = {"record_id", "provider", "model", "api_version", "request_id",
"observed_at", "latency_ms", "response"}
for record in records:
if set(record) != fields or not start <= timestamp(record["observed_at"]) < end:
raise ValueError("record schema or window binding differs")
number(record["latency_ms"], "latency_ms")
return records, receipt, caps, policy
def select_profile(record: dict, profiles: list[dict]) -> dict | None:
matches = [item for item in profiles if isinstance(item, dict)
and item.get("provider") == record["provider"]
and item.get("model") == record["model"]
and item.get("api_version") == record["api_version"]]
if len(matches) > 1:
raise ValueError("ambiguous capability profile")
return matches[0] if matches else None
def evaluate(record: dict, profiles: list[dict], policy: dict) -> dict:
profile = select_profile(record, profiles)
base = {"record_id": record["record_id"], "request_id": record["request_id"],
"provider": record["provider"], "model": record["model"],
"api_version": record["api_version"], "observed_at": record["observed_at"],
"latency_ms_join_only": record["latency_ms"]}
if profile is None or profile.get("applicable") is False:
return {**base, "control_status": "NOT_APPLICABLE",
"finding_status": "NOT_EVALUATED", "presence": {},
"reason": "no signed applicable capability profile"}
expected = {"provider", "model", "api_version", "applicable", "paths", "required_fields"}
if set(profile) != expected or profile["applicable"] is not True:
raise ValueError("applicable capability profile is malformed")
paths, required = profile["paths"], profile["required_fields"]
allowed = {"thinking_tokens", "visible_output_tokens", "budget_tokens", "display_mode",
"redacted_block_count", "omitted_reasoning", "trace_summary"}
if (not isinstance(paths, dict) or not set(paths).issubset(allowed)
or not isinstance(required, list) or not set(required).issubset(set(paths))):
raise ValueError("capability paths are invalid")
extracted = {name: pointer(record["response"], path) for name, path in paths.items()}
presence = {name: present for name, (present, _value) in extracted.items()}
missing = sorted(name for name in required if not presence[name])
if missing:
return {**base, "control_status": "INSUFFICIENT_DATA",
"finding_status": "UNKNOWN", "presence": presence,
"missing_fields": missing, "reason": "advertised fields are absent"}
try:
values = {name: value for name, (present, value) in extracted.items() if present}
for field in ("thinking_tokens", "visible_output_tokens", "budget_tokens", "redacted_block_count"):
if field in values:
values[field] = number(values[field], field)
if "omitted_reasoning" in values and not isinstance(values["omitted_reasoning"], bool):
raise ValueError("omitted_reasoning is not Boolean")
if "display_mode" in values and not isinstance(values["display_mode"], str):
raise ValueError("display_mode is not a string")
if "trace_summary" in values and not isinstance(values["trace_summary"], str):
raise ValueError("trace_summary is not a string")
except ValueError as error:
return {**base, "control_status": "ERROR",
"finding_status": "UNKNOWN", "presence": presence,
"reason": str(error)}
findings: list[str] = []
hidden, visible, budget = values.get("thinking_tokens"), values.get("visible_output_tokens"), values.get("budget_tokens")
if hidden is not None and budget is not None and hidden > budget:
findings.append("thinking_budget_overrun")
if hidden is not None and visible is not None:
if visible == 0 and hidden >= policy["min_hidden_tokens_without_output"]:
findings.append("hidden_reasoning_without_visible_output")
elif visible > 0 and hidden / visible > policy["max_hidden_visible_ratio"]:
findings.append("high_hidden_visible_ratio")
if values.get("display_mode") == "raw":
findings.append("raw_reasoning_trace_displayed")
if "redacted_block_count" in values and values["redacted_block_count"] >= policy["max_redacted_blocks"]:
findings.append("repeated_reasoning_redaction")
summary_chars = len(values["trace_summary"]) if "trace_summary" in values else None
if summary_chars is not None and summary_chars > policy["max_summary_chars"]:
findings.append("reasoning_summary_length_anomaly")
return {**base, "control_status": "PASS",
"finding_status": "FINDING" if findings else "NO_FINDING",
"presence": presence,
"findings": sorted(findings), "summary_chars": summary_chars,
"thinking_tokens": hidden, "visible_output_tokens": visible,
"budget_tokens": budget, "redacted_block_count": values.get("redacted_block_count"),
"omitted_reasoning": values.get("omitted_reasoning"), "display_mode": values.get("display_mode")}
def main() -> None:
try:
records, receipt, caps, policy = load()
results = [evaluate(record, caps["profiles"], policy) for record in records]
eligible = sum(
result["control_status"] != "NOT_APPLICABLE"
for result in results
)
if eligible != receipt["eligible_record_count"]:
raise ValueError("eligible population differs from completeness receipt")
order = {
"NOT_APPLICABLE": 0, "PASS": 1,
"INSUFFICIENT_DATA": 2, "ERROR": 3,
}
control_status = max(
(item["control_status"] for item in results),
key=order.get,
default="INSUFFICIENT_DATA",
)
finding_status = (
"UNKNOWN"
if control_status not in {"PASS", "NOT_APPLICABLE"}
else "FINDING"
if any(item["finding_status"] == "FINDING" for item in results)
else "NO_FINDING"
if eligible
else "NOT_EVALUATED"
)
evidence = {"schema_version": "aidefend.d002004-evidence.v1", "control": "AID-D-002.004",
"control_status": control_status,
"finding_status": finding_status,
"receipt_sha256": sha256_file(RECEIPT),
"records_sha256": sha256_file(RECORDS), "capability_sha256": sha256_file(CAPS),
"capability_version": caps["profile_version"], "policy_sha256": sha256_file(POLICY),
"policy_version": policy["policy_version"], "evaluated_population": len(results),
"results": results, "evaluated_at": datetime.now(timezone.utc).isoformat()}
except Exception as error:
evidence = {"schema_version": "aidefend.d002004-evidence.v1", "control": "AID-D-002.004",
"control_status": "ERROR", "finding_status": "UNKNOWN",
"reason": f"{type(error).__name__}: {error}",
"evaluated_at": datetime.now(timezone.utc).isoformat()}
EVIDENCE.parent.mkdir(parents=True, exist_ok=True)
EVIDENCE.write_text(json.dumps(evidence, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
subprocess.run(["cosign", "sign-blob", "--yes", "--key", "env://D002004_EVIDENCE_SIGNING_KEY",
"--bundle", str(EVIDENCE) + ".sig", str(EVIDENCE)], check=True, timeout=REASONING_TELEMETRY_COMMAND_TIMEOUT_SECONDS)
verify(EVIDENCE, "D002004_EVIDENCE_VERIFY_KEY")
if __name__ == "__main__":
main()
Interpret results and retain evidence
Action: deploy the adapter at the provider-response observation point, require exact capability and completeness receipts, and forward signed findings to SIEM. Do not block traffic or turn absent provider fields into a clean result.
Verify safely
An independent verifier must re-extract fields from immutable sanitized response envelopes and recompute reasoning ratios, budget findings, disclosure findings, and population states, read evidence back, verify signatures and digests, and compare the canonical result digest.
Open-source tools
- OpenTelemetry
- Prometheus
- Grafana
- ClickHouse
- LiteLLM
- Langfuse (provider-reported reasoning-token and trace telemetry; hidden reasoning remains unavailable unless provider-exposed)
- Cosign
Commercial / hosted tools
- Datadog
- Splunk Enterprise Security
- New Relic
- Cloudflare AI Gateway
- Amazon Bedrock
Threat mappings
MITRE ATLAS
- AML.T0034 Cost Harvesting
- AML.T0034.001 Cost Harvesting: Resource-Intensive Queries
- AML.T0057 LLM Data Leakage (reasoning-trace telemetry detects unexpected hidden-trace disclosure)
MAESTRO
- Resource Hijacking (L4)
- Data Leakage through Observability (L5) (reasoning telemetry detects exposed thinking blocks and trace summaries)
OWASP LLM Top 10 2026
- LLM02:2026 Sensitive Information Disclosure
- LLM06:2026 Unbounded Consumption
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- NISTAML.014 Energy-latency
- NISTAML.036 Leaking information from user interactions (detects reasoning-trace leakage returned through model interactions)
Cisco Integrated AI Security and Safety Framework
- AISubtech-13.1.1 Compute Exhaustion (reasoning-token growth and token-ratio thresholds)
- AISubtech-13.1.3 Model Denial of Service (reasoning-token growth and token-ratio thresholds)
- AISubtech-13.2.1 Service Misuse for Cost Inflation (reasoning-token growth and token-ratio thresholds)
- AITech-13.1 Disruption of Availability (reasoning-token growth and token-ratio thresholds)
- AITech-13.2 Cost Harvesting / Repurposing (reasoning-token growth and token-ratio thresholds)
- AITech-8.2 Data Exfiltration / Exposure (verified provider display metadata exposes raw reasoning; sensitive content is not independently classified)
- AITech-8.3 Information Disclosure (verified provider display metadata exposes raw reasoning; sensitive content is not independently classified)
Google Secure AI Framework 2.0 - Risks
- DMS: Denial of ML Service
- SDD: Sensitive Data Disclosure (reasoning telemetry detects unintended hidden-trace disclosure)
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.7: Denial of Service (DoS)
- Agents - Core 13.4: Resource Overload
- Model Serving - Inference response 10.6: Sensitive data output from a model (detects sensitive content exposed through reasoning traces)