mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Merge remote-tracking branch 'upstream/main' into pr2145
This commit is contained in:
commit
05e9ee4b9b
3 changed files with 112 additions and 12 deletions
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
- **memory:** annotate `_EMBEDDER_CACHE` as `dict[tuple[str, str, str], Embedder]` to match the 3-element key (backend, model, ollama_base_url). The stale 2-tuple annotation made `mypy headroom` fail on `main`, which broke the `lint` CI job on every open PR.
|
||||
- **install:** include `orjson` in the `[proxy]` extra so `uv tool install "headroom-ai[all]"` satisfies LiteLLM OpenRouter/provider backends that import it at runtime ([#2056](https://github.com/headroomlabs-ai/headroom/issues/2056)).
|
||||
- The dashboard's per-request metadata (the `recent_requests` / `request_logs`
|
||||
tail and the `config` block with upstream URLs) is gated to loopback callers
|
||||
|
|
|
|||
41
headroom/cache/dynamic_detector.py
vendored
41
headroom/cache/dynamic_detector.py
vendored
|
|
@ -300,8 +300,15 @@ class RegexDetector:
|
|||
DynamicCategory.REQUEST_ID,
|
||||
"api_key",
|
||||
),
|
||||
# Common prefixed IDs (req_, sess_, txn_, etc.)
|
||||
(r"\b[a-z]{2,6}_[a-zA-Z0-9]{8,}", DynamicCategory.REQUEST_ID, "prefixed_id"),
|
||||
# Common prefixed IDs (req_, sess_, txn_, etc.). The suffix must
|
||||
# contain at least one digit (lookahead) so genuine generated ids
|
||||
# like "req_a1b2c3d4" match while plain snake_case compound words
|
||||
# like "in_progress" or "is_valid" (all letters, no digit) do not.
|
||||
(
|
||||
r"\b[a-z]{2,6}_(?=[a-zA-Z0-9]*\d)[a-zA-Z0-9]{8,}",
|
||||
DynamicCategory.REQUEST_ID,
|
||||
"prefixed_id",
|
||||
),
|
||||
# Hex strings of common ID lengths (32 = MD5, 40 = SHA1, 64 = SHA256)
|
||||
(r"\b[a-fA-F0-9]{32}\b", DynamicCategory.IDENTIFIER, "hex_32"),
|
||||
(r"\b[a-fA-F0-9]{40}\b", DynamicCategory.IDENTIFIER, "hex_40"),
|
||||
|
|
@ -325,10 +332,20 @@ class RegexDetector:
|
|||
]
|
||||
|
||||
# Build structural pattern from dynamic labels
|
||||
# Pattern: "label" followed by separator then value
|
||||
# Pattern: "label" followed by an explicit key/value separator then value.
|
||||
#
|
||||
# Two constraints keep this from firing on ordinary prose and code:
|
||||
# * A word boundary (\b) and a trailing negative-lookahead on word
|
||||
# characters anchor the label as a whole word. Without them a label
|
||||
# like "token" or "last" matched as a substring inside unrelated
|
||||
# identifiers such as "getAuthToken" or "blast".
|
||||
# * The separator must be an explicit ":" or "=" (optionally spaced).
|
||||
# A bare-whitespace separator turned any English sentence beginning
|
||||
# with a label word ("current work is ...", "name of the file ...")
|
||||
# into a bogus label/value pair that swallowed the rest of the clause.
|
||||
labels_pattern = "|".join(re.escape(label) for label in config.dynamic_labels)
|
||||
self._structural_pattern = re.compile(
|
||||
rf"(?P<label>(?:{labels_pattern}))(?P<sep>\s*[:=]\s*|\s+)(?P<value>[^\n,;]+)",
|
||||
rf"\b(?P<label>(?:{labels_pattern}))(?!\w)(?P<sep>\s*[:=]\s*)(?P<value>[^\n,;]+)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
|
@ -459,12 +476,18 @@ class RegexDetector:
|
|||
if len(text) < self.config.min_entropy_length:
|
||||
continue
|
||||
|
||||
# Skip if all letters or all numbers (not random-looking)
|
||||
if text.isalpha() or text.isdigit():
|
||||
# Require genuinely id-shaped structure rather than a random-looking
|
||||
# spelling. Generated identifiers (session ids, request ids, hashes,
|
||||
# tokens) essentially always mix in at least one digit, whereas
|
||||
# ordinary words and compound identifiers are letters (plus "-"/"_"
|
||||
# separators) only. Skipping the letters-only case avoids flagging
|
||||
# prose words as well as snake_case / kebab-case vocabulary like
|
||||
# "in_progress", "system-reminder" or "total_tokens" that recurs
|
||||
# identically every turn and must stay in the cacheable prefix.
|
||||
if text.isdigit():
|
||||
continue
|
||||
|
||||
# Skip common words that might look like IDs
|
||||
if text.lower() in {"username", "password", "localhost", "undefined"}:
|
||||
letters_only = text.replace("-", "").replace("_", "")
|
||||
if not letters_only or letters_only.isalpha():
|
||||
continue
|
||||
|
||||
# Calculate entropy
|
||||
|
|
|
|||
|
|
@ -85,10 +85,16 @@ class TestRegexDetector:
|
|||
assert spans[0].category == DynamicCategory.VERSION
|
||||
|
||||
def test_date_prefix_pattern(self, detector):
|
||||
"""Test full date prefix phrase detection."""
|
||||
spans = detector.detect("Today is Monday, January 15, 2024. You are an assistant.")
|
||||
"""Test labeled date phrase detection.
|
||||
|
||||
Structural detection requires an explicit ``:``/``=`` separator (a
|
||||
bare-whitespace separator used to swallow ordinary prose such as
|
||||
"Today is Monday..." — see issue #2110). With the label properly
|
||||
delimited, the locale-formatted date value is still extracted.
|
||||
"""
|
||||
spans = detector.detect("Today: Monday, January 15, 2024. You are an assistant.")
|
||||
assert len(spans) >= 1
|
||||
# Should detect the full phrase
|
||||
# Should detect the labeled value
|
||||
date_spans = [s for s in spans if s.category == DynamicCategory.DATE]
|
||||
assert len(date_spans) >= 1
|
||||
|
||||
|
|
@ -287,6 +293,76 @@ class TestEntropyDetection:
|
|||
assert "password" not in flagged_words
|
||||
|
||||
|
||||
class TestIssue2110FalsePositives:
|
||||
"""Regression tests for issue #2110.
|
||||
|
||||
The detector misclassified ordinary English words and code identifiers
|
||||
(e.g. ``in_progress``, ``is_valid``, ``getAuthToken``) as dynamic content,
|
||||
extracting them from the system prompt and re-appending them as a growing
|
||||
``[Dynamic Context]`` tail that corrupted the cached prefix. Genuinely
|
||||
dynamic *shapes* (UUIDs, timestamps, hashes, prefixed ids with a digit)
|
||||
must still be detected.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def detector(self):
|
||||
return DynamicContentDetector(DetectorConfig(tiers=["regex"]))
|
||||
|
||||
# --- must NOT be flagged (the reported false positives) ------------------
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"in_progress", # snake_case status word (prefixed_id false positive)
|
||||
"is_valid", # snake_case identifier (entropy false positive)
|
||||
"in_pr", # ordinary short token
|
||||
"total_tokens", # snake_case compound word
|
||||
"system-reminder", # kebab-case tag name
|
||||
"getAuthToken (function - src/services/firebase.ts:92)", # code identifier + path
|
||||
"DebugModal (function - src/components/layout/DebugModal.tsx:11)",
|
||||
"The current work is being done", # prose starting with a label word
|
||||
"last updated the file yesterday", # prose starting with a label word
|
||||
"the user should review this", # prose containing a label word
|
||||
"the name of the file is unknown", # prose containing a label word
|
||||
],
|
||||
)
|
||||
def test_ordinary_words_and_identifiers_not_extracted(self, detector, text):
|
||||
result = detector.detect(text)
|
||||
assert result.spans == [], f"unexpected dynamic spans for {text!r}: {result.spans}"
|
||||
# Nothing extracted -> the static content is preserved verbatim and the
|
||||
# dynamic tail stays empty (so it can't grow over a session).
|
||||
assert result.dynamic_content == ""
|
||||
|
||||
# --- MUST still be flagged (genuinely dynamic shapes) --------------------
|
||||
|
||||
def test_uuid_still_detected(self, detector):
|
||||
text = "550e8400-e29b-41d4-a716-446655440000"
|
||||
spans = detector.detect(text).spans
|
||||
assert any(s.category == DynamicCategory.UUID and s.text == text for s in spans)
|
||||
|
||||
def test_timestamp_still_detected(self, detector):
|
||||
spans = detector.detect("event at 2026-07-12T10:30:00Z happened").spans
|
||||
assert any(s.text == "2026-07-12T10:30:00Z" for s in spans)
|
||||
|
||||
def test_long_hex_hash_still_detected(self, detector):
|
||||
sha1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
||||
spans = detector.detect(sha1).spans
|
||||
assert any(s.category == DynamicCategory.IDENTIFIER and s.text == sha1 for s in spans)
|
||||
|
||||
def test_prefixed_id_with_digit_still_detected(self, detector):
|
||||
spans = detector.detect("req_a1b2c3d4").spans
|
||||
assert any(s.category == DynamicCategory.REQUEST_ID for s in spans)
|
||||
|
||||
def test_labeled_dynamic_value_still_detected(self, detector):
|
||||
# Explicit "label: value" — the label stays static, the value is dynamic.
|
||||
spans = detector.detect("session_id: 8f3e2a1c9d").spans
|
||||
assert any(s.text == "8f3e2a1c9d" for s in spans)
|
||||
|
||||
def test_high_entropy_id_with_digits_still_detected(self, detector):
|
||||
spans = detector.detect("a1b2c3d4e5f6g7h8").spans
|
||||
assert any(s.category == DynamicCategory.IDENTIFIER for s in spans)
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases and tricky inputs."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue