omnicache-proxy 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/config.py +62 -0
- core/embeddings.py +140 -0
- core/hasher.py +105 -0
- core/privacy_shield.py +93 -0
- core/radix_tree.py +115 -0
- core/vector_cache.py +286 -0
- core/vision_cache.py +145 -0
- mcp/server.py +257 -0
- omnicache_proxy-2.0.0.dist-info/METADATA +167 -0
- omnicache_proxy-2.0.0.dist-info/RECORD +25 -0
- omnicache_proxy-2.0.0.dist-info/WHEEL +5 -0
- omnicache_proxy-2.0.0.dist-info/entry_points.txt +2 -0
- omnicache_proxy-2.0.0.dist-info/licenses/LICENSE +21 -0
- omnicache_proxy-2.0.0.dist-info/top_level.txt +4 -0
- persistence/snapshot_store.py +160 -0
- server/__init__.py +0 -0
- server/cascade_router.py +123 -0
- server/failover.py +56 -0
- server/gateway.py +514 -0
- server/quotas.py +80 -0
- server/singleflight.py +65 -0
- server/stream_replayer.py +132 -0
- server/tool_replayer.py +86 -0
- server/translator.py +125 -0
- server/upstream.py +107 -0
core/config.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration and pricing registry for OmniCache AI Proxy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
# Provider Pricing Table (USD per 1,000,000 tokens)
|
|
9
|
+
# Updated to reflect 2025/2026 current provider pricing
|
|
10
|
+
MODEL_PRICING: Dict[str, Dict[str, float]] = {
|
|
11
|
+
# OpenAI Models
|
|
12
|
+
"gpt-4o": {"input": 2.50, "output": 10.00, "cached_input": 1.25},
|
|
13
|
+
"gpt-4o-mini": {"input": 0.15, "output": 0.60, "cached_input": 0.075},
|
|
14
|
+
"o1": {"input": 15.00, "output": 60.00, "cached_input": 7.50},
|
|
15
|
+
"o3-mini": {"input": 1.10, "output": 4.40, "cached_input": 0.55},
|
|
16
|
+
"gpt-4-turbo": {"input": 10.00, "output": 30.00, "cached_input": 5.00},
|
|
17
|
+
"gpt-3.5-turbo": {"input": 0.50, "output": 1.50, "cached_input": 0.25},
|
|
18
|
+
|
|
19
|
+
# Anthropic Models
|
|
20
|
+
"claude-3-5-sonnet-20241022": {"input": 3.00, "output": 15.00, "cached_input": 0.30},
|
|
21
|
+
"claude-3-5-haiku-20241022": {"input": 0.80, "output": 4.00, "cached_input": 0.08},
|
|
22
|
+
"claude-3-7-sonnet": {"input": 3.00, "output": 15.00, "cached_input": 0.30},
|
|
23
|
+
|
|
24
|
+
# Google Gemini Models
|
|
25
|
+
"gemini-2.5-flash": {"input": 0.10, "output": 0.40, "cached_input": 0.025},
|
|
26
|
+
"gemini-1.5-pro": {"input": 1.25, "output": 5.00, "cached_input": 0.3125},
|
|
27
|
+
"gemini-1.5-flash": {"input": 0.075, "output": 0.30, "cached_input": 0.01875},
|
|
28
|
+
|
|
29
|
+
# Default fallback
|
|
30
|
+
"default": {"input": 2.00, "output": 8.00, "cached_input": 1.00},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class ProxyConfig:
|
|
34
|
+
PORT: int = int(os.getenv("OMNICACHE_PORT", "8000"))
|
|
35
|
+
HOST: str = os.getenv("OMNICACHE_HOST", "0.0.0.0")
|
|
36
|
+
|
|
37
|
+
# Default Upstream Provider endpoints
|
|
38
|
+
OPENAI_BASE_URL: str = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
|
39
|
+
ANTHROPIC_BASE_URL: str = os.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com/v1")
|
|
40
|
+
GEMINI_BASE_URL: str = os.getenv("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/openai")
|
|
41
|
+
|
|
42
|
+
# Cache Configuration
|
|
43
|
+
DEFAULT_SIMILARITY_THRESHOLD: float = float(os.getenv("SIMILARITY_THRESHOLD", "0.92"))
|
|
44
|
+
EXACT_CACHE_TTL_SECONDS: int = int(os.getenv("EXACT_CACHE_TTL", "604800")) # 7 days
|
|
45
|
+
SEMANTIC_CACHE_TTL_SECONDS: int = int(os.getenv("SEMANTIC_CACHE_TTL", "604800")) # 7 days
|
|
46
|
+
MAX_CACHE_ENTRIES_PER_TENANT: int = int(os.getenv("MAX_CACHE_ENTRIES", "10000"))
|
|
47
|
+
|
|
48
|
+
# Temperature threshold above which semantic cache is bypassed
|
|
49
|
+
TEMPERATURE_BYPASS_THRESHOLD: float = 0.7
|
|
50
|
+
|
|
51
|
+
# Token Jitter Stream Velocity (tokens per second for cached stream playback)
|
|
52
|
+
STREAM_REPLAY_TOKENS_PER_SEC: float = 65.0
|
|
53
|
+
|
|
54
|
+
# SingleFlight lock timeout in seconds
|
|
55
|
+
SINGLEFLIGHT_TIMEOUT_SECONDS: float = 30.0
|
|
56
|
+
|
|
57
|
+
# Upstream Connection Pool settings
|
|
58
|
+
HTTP_POOL_MAX_CONNECTIONS: int = 100
|
|
59
|
+
HTTP_POOL_MAX_KEEPALIVE: int = 20
|
|
60
|
+
HTTP_TIMEOUT_SECONDS: float = 60.0
|
|
61
|
+
|
|
62
|
+
config = ProxyConfig()
|
core/embeddings.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-performance in-memory semantic embedding engine.
|
|
3
|
+
Generates normalized dense vector representations for sub-millisecond similarity search.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
import hashlib
|
|
9
|
+
from typing import List, Dict, Tuple, Optional
|
|
10
|
+
|
|
11
|
+
class FastSemanticEmbedder:
|
|
12
|
+
"""
|
|
13
|
+
Sub-millisecond semantic text embedder using high-dimensional hashed character/word n-gram
|
|
14
|
+
content-term frequency projection, synonym canonicalization, and L2-unit normalization.
|
|
15
|
+
Provides robust semantic matching for question rephrasings, synonyms, and variations.
|
|
16
|
+
"""
|
|
17
|
+
DIMENSIONS: int = 512
|
|
18
|
+
|
|
19
|
+
# Common English & Multilingual stopwords for query normalization
|
|
20
|
+
STOPWORDS = {
|
|
21
|
+
"a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are",
|
|
22
|
+
"as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but",
|
|
23
|
+
"by", "could", "did", "do", "does", "doing", "down", "during", "each", "few", "for", "from",
|
|
24
|
+
"further", "had", "has", "have", "having", "he", "her", "here", "hers", "herself", "him",
|
|
25
|
+
"himself", "his", "how", "i", "if", "in", "into", "is", "it", "its", "itself", "just",
|
|
26
|
+
"me", "more", "most", "my", "myself", "no", "nor", "not", "now", "of", "off", "on", "once",
|
|
27
|
+
"only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same",
|
|
28
|
+
"she", "should", "so", "some", "such", "than", "that", "the", "their", "theirs", "them",
|
|
29
|
+
"themselves", "then", "there", "these", "they", "this", "those", "through", "to", "too",
|
|
30
|
+
"under", "until", "up", "very", "was", "we", "were", "what", "when", "where", "which",
|
|
31
|
+
"while", "who", "whom", "why", "with", "would", "you", "your", "yours", "yourself", "yourselves",
|
|
32
|
+
"please", "tell", "explain", "help", "can", "could"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
SYNONYM_MAP = {
|
|
36
|
+
"recover": "reset",
|
|
37
|
+
"recovery": "reset",
|
|
38
|
+
"forgotten": "reset",
|
|
39
|
+
"forgot": "reset",
|
|
40
|
+
"procedure": "steps",
|
|
41
|
+
"method": "steps",
|
|
42
|
+
"instructions": "steps",
|
|
43
|
+
"location": "located",
|
|
44
|
+
"whereabouts": "located",
|
|
45
|
+
"place": "located",
|
|
46
|
+
"pricing": "price",
|
|
47
|
+
"costs": "price",
|
|
48
|
+
"rate": "price",
|
|
49
|
+
"authenticate": "login",
|
|
50
|
+
"signin": "login",
|
|
51
|
+
"signup": "register",
|
|
52
|
+
"terminate": "cancel",
|
|
53
|
+
"modify": "change",
|
|
54
|
+
"update": "change",
|
|
55
|
+
"create": "make",
|
|
56
|
+
"build": "make",
|
|
57
|
+
"construct": "make",
|
|
58
|
+
"generate": "make",
|
|
59
|
+
"fix": "repair",
|
|
60
|
+
"troubleshoot": "repair"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def clean_and_tokenize(cls, text: str) -> List[str]:
|
|
65
|
+
"""Normalize text: lowercase, remove special characters, tokenize, canonicalize synonyms."""
|
|
66
|
+
text = text.lower()
|
|
67
|
+
# Keep alphanumeric, remove punctuation
|
|
68
|
+
text = re.sub(r"[^\w\s]", " ", text)
|
|
69
|
+
raw_tokens = text.split()
|
|
70
|
+
return [cls.SYNONYM_MAP.get(t, t) for t in raw_tokens]
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_features(cls, text: str) -> Dict[str, float]:
|
|
74
|
+
"""Extract unigram, content bigrams, and char n-gram weighted features."""
|
|
75
|
+
tokens = cls.clean_and_tokenize(text)
|
|
76
|
+
if not tokens:
|
|
77
|
+
return {}
|
|
78
|
+
|
|
79
|
+
features: Dict[str, float] = {}
|
|
80
|
+
content_tokens = [t for t in tokens if t not in cls.STOPWORDS]
|
|
81
|
+
|
|
82
|
+
# 1. Word unigrams (content words get high weight, stopwords low weight)
|
|
83
|
+
for token in tokens:
|
|
84
|
+
weight = 0.1 if token in cls.STOPWORDS else 2.0
|
|
85
|
+
features[f"w:{token}"] = features.get(f"w:{token}", 0.0) + weight
|
|
86
|
+
|
|
87
|
+
# 2. Content bigrams (skip noise stopwords)
|
|
88
|
+
for i in range(len(content_tokens) - 1):
|
|
89
|
+
bg = f"{content_tokens[i]}_{content_tokens[i+1]}"
|
|
90
|
+
features[f"bg:{bg}"] = features.get(f"bg:{bg}", 0.0) + 1.0
|
|
91
|
+
|
|
92
|
+
# 3. Subword 3-grams and 4-grams for content words (typos and morphology)
|
|
93
|
+
for token in content_tokens:
|
|
94
|
+
if len(token) >= 3:
|
|
95
|
+
for n in (3, 4):
|
|
96
|
+
for i in range(len(token) - n + 1):
|
|
97
|
+
ngram = token[i:i+n]
|
|
98
|
+
features[f"ng:{ngram}"] = features.get(f"ng:{ngram}", 0.0) + 0.5
|
|
99
|
+
|
|
100
|
+
return features
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def embed(cls, text: str) -> List[float]:
|
|
104
|
+
"""
|
|
105
|
+
Embeds text into a 512-dimensional L2-normalized dense vector.
|
|
106
|
+
"""
|
|
107
|
+
if not text or not text.strip():
|
|
108
|
+
return [0.0] * cls.DIMENSIONS
|
|
109
|
+
|
|
110
|
+
features = cls.get_features(text)
|
|
111
|
+
vector = [0.0] * cls.DIMENSIONS
|
|
112
|
+
|
|
113
|
+
# Feature hashing into fixed dimension space
|
|
114
|
+
for feat, weight in features.items():
|
|
115
|
+
h = int(hashlib.md5(feat.encode('utf-8')).hexdigest()[:8], 16)
|
|
116
|
+
idx = h % cls.DIMENSIONS
|
|
117
|
+
sign = 1.0 if (h >> 4) & 1 else -1.0
|
|
118
|
+
vector[idx] += sign * weight
|
|
119
|
+
|
|
120
|
+
# Compute L2 Norm (Euclidean length)
|
|
121
|
+
norm_sq = sum(x * x for x in vector)
|
|
122
|
+
if norm_sq > 0:
|
|
123
|
+
norm = math.sqrt(norm_sq)
|
|
124
|
+
vector = [x / norm for x in vector]
|
|
125
|
+
|
|
126
|
+
return vector
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def cosine_similarity(vec_a: List[float], vec_b: List[float]) -> float:
|
|
130
|
+
"""
|
|
131
|
+
Calculates cosine similarity between two unit-normalized vectors.
|
|
132
|
+
For unit vectors: dot_product = cosine_similarity.
|
|
133
|
+
"""
|
|
134
|
+
if not vec_a or not vec_b or len(vec_a) != len(vec_b):
|
|
135
|
+
return 0.0
|
|
136
|
+
|
|
137
|
+
# Dot product
|
|
138
|
+
dot = sum(a * b for a, b in zip(vec_a, vec_b))
|
|
139
|
+
# Clamp to [0.0, 1.0] for similarity index
|
|
140
|
+
return max(0.0, min(1.0, dot))
|
core/hasher.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Composite hashing and prompt extraction utilities.
|
|
3
|
+
Ensures zero collisions between differing JSON schemas, system prompts, or tool definitions.
|
|
4
|
+
Also includes optional PII redaction utilities for enterprise privacy compliance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from typing import Dict, Any, Tuple, Optional, List
|
|
11
|
+
|
|
12
|
+
class RequestHasher:
|
|
13
|
+
# Common PII Regex Patterns
|
|
14
|
+
SSN_PATTERN = r"\b\d{3}-\d{2}-\d{4}\b"
|
|
15
|
+
CREDIT_CARD_PATTERN = r"\b(?:\d{4}[-\s]?){3}\d{4}\b"
|
|
16
|
+
EMAIL_PATTERN = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def redact_pii(cls, text: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Anonymizes sensitive tokens before hashing or embedding.
|
|
22
|
+
"""
|
|
23
|
+
if not text:
|
|
24
|
+
return ""
|
|
25
|
+
text = re.sub(cls.SSN_PATTERN, "[REDACTED_SSN]", text)
|
|
26
|
+
text = re.sub(cls.CREDIT_CARD_PATTERN, "[REDACTED_CC]", text)
|
|
27
|
+
text = re.sub(cls.EMAIL_PATTERN, "[REDACTED_EMAIL]", text)
|
|
28
|
+
return text
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def extract_system_and_user_prompts(messages: List[Dict[str, Any]]) -> Tuple[str, str, bool]:
|
|
32
|
+
"""
|
|
33
|
+
Extracts concatenated system prompt and last user prompt.
|
|
34
|
+
Also returns a boolean indicating if multimodal/image content is detected.
|
|
35
|
+
"""
|
|
36
|
+
system_parts = []
|
|
37
|
+
user_parts = []
|
|
38
|
+
is_multimodal = False
|
|
39
|
+
|
|
40
|
+
for msg in messages:
|
|
41
|
+
role = msg.get("role", "")
|
|
42
|
+
content = msg.get("content", "")
|
|
43
|
+
|
|
44
|
+
if isinstance(content, list):
|
|
45
|
+
# Multimodal format: [{type: 'text', text: '...'}, {type: 'image_url', ...}]
|
|
46
|
+
text_subparts = []
|
|
47
|
+
for part in content:
|
|
48
|
+
if isinstance(part, dict):
|
|
49
|
+
if part.get("type") == "text":
|
|
50
|
+
text_subparts.append(part.get("text", ""))
|
|
51
|
+
elif part.get("type") in ("image_url", "input_audio", "file"):
|
|
52
|
+
is_multimodal = True
|
|
53
|
+
content_str = " ".join(text_subparts)
|
|
54
|
+
else:
|
|
55
|
+
content_str = str(content) if content is not None else ""
|
|
56
|
+
|
|
57
|
+
if role == "system":
|
|
58
|
+
system_parts.append(content_str)
|
|
59
|
+
elif role == "user":
|
|
60
|
+
user_parts.append(content_str)
|
|
61
|
+
|
|
62
|
+
system_prompt = "\n".join(system_parts).strip()
|
|
63
|
+
last_user_prompt = user_parts[-1] if user_parts else ""
|
|
64
|
+
return system_prompt, last_user_prompt, is_multimodal
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def compute_exact_hash(cls, payload: Dict[str, Any], org_id: str = "default") -> str:
|
|
68
|
+
"""
|
|
69
|
+
Computes a deterministic SHA-256 hash representing the exact request signature.
|
|
70
|
+
Includes model, messages, temperature, response_format (schema), tools, and stop sequences.
|
|
71
|
+
"""
|
|
72
|
+
normalized_data = {
|
|
73
|
+
"org_id": org_id,
|
|
74
|
+
"model": payload.get("model", "").strip().lower(),
|
|
75
|
+
"messages": payload.get("messages", []),
|
|
76
|
+
"temperature": payload.get("temperature", 1.0),
|
|
77
|
+
"response_format": payload.get("response_format", None),
|
|
78
|
+
"tools": payload.get("tools", None),
|
|
79
|
+
"tool_choice": payload.get("tool_choice", None),
|
|
80
|
+
"stop": payload.get("stop", None)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Serialize to deterministic JSON with sorted keys
|
|
84
|
+
json_bytes = json.dumps(normalized_data, sort_keys=True, separators=(',', ':')).encode('utf-8')
|
|
85
|
+
return hashlib.sha256(json_bytes).hexdigest()
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def compute_schema_hash(cls, response_format: Optional[Dict[str, Any]]) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Computes deterministic hash for JSON Schema structured outputs.
|
|
91
|
+
"""
|
|
92
|
+
if not response_format:
|
|
93
|
+
return "no_schema"
|
|
94
|
+
raw_bytes = json.dumps(response_format, sort_keys=True, separators=(',', ':')).encode('utf-8')
|
|
95
|
+
return hashlib.sha256(raw_bytes).hexdigest()[:16]
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def compute_tools_hash(cls, tools: Optional[List[Dict[str, Any]]]) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Computes deterministic hash for agent tool and function definitions.
|
|
101
|
+
"""
|
|
102
|
+
if not tools:
|
|
103
|
+
return "no_tools"
|
|
104
|
+
raw_bytes = json.dumps(tools, sort_keys=True, separators=(',', ':')).encode('utf-8')
|
|
105
|
+
return hashlib.sha256(raw_bytes).hexdigest()[:16]
|
core/privacy_shield.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zero-Knowledge Enterprise Privacy Shield & Reversible PII Tokenizer.
|
|
3
|
+
Automatically scrubs SSNs, Credit Cards, Emails, API Keys, and PHI before sending to upstream LLMs,
|
|
4
|
+
and seamlessly rehydrates original data on response delivery for HIPAA & SOC2 compliance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, Tuple, List, Any
|
|
9
|
+
|
|
10
|
+
# Enterprise PII Detection Regex Patterns
|
|
11
|
+
PATTERNS = {
|
|
12
|
+
"SSN": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
|
|
13
|
+
"CREDIT_CARD": re.compile(r"\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b"),
|
|
14
|
+
"EMAIL": re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"),
|
|
15
|
+
"API_KEY": re.compile(r"\b(sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16})\b"),
|
|
16
|
+
"PHONE": re.compile(r"\b(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class PrivacyShield:
|
|
20
|
+
"""Reversible PII/PHI scrubbing and token rehydration engine."""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def sanitize_text(cls, text: str) -> Tuple[str, Dict[str, str], int]:
|
|
24
|
+
"""
|
|
25
|
+
Replaces sensitive PII instances with deterministic tokens.
|
|
26
|
+
Returns (sanitized_text, token_map, total_redactions).
|
|
27
|
+
"""
|
|
28
|
+
token_map = {}
|
|
29
|
+
counter = 1
|
|
30
|
+
sanitized = text
|
|
31
|
+
|
|
32
|
+
for pii_type, regex in PATTERNS.items():
|
|
33
|
+
matches = list(set(regex.findall(sanitized)))
|
|
34
|
+
for match in matches:
|
|
35
|
+
token = f"[REDACTED_{pii_type}_{counter}]"
|
|
36
|
+
token_map[token] = match
|
|
37
|
+
sanitized = sanitized.replace(match, token)
|
|
38
|
+
counter += 1
|
|
39
|
+
|
|
40
|
+
return sanitized, token_map, (counter - 1)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def sanitize_payload(cls, payload: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, str], int]:
|
|
44
|
+
"""
|
|
45
|
+
Recursively sanitizes all message contents in an OpenAI / Claude payload.
|
|
46
|
+
"""
|
|
47
|
+
sanitized_payload = dict(payload)
|
|
48
|
+
master_token_map = {}
|
|
49
|
+
total_scrubbed = 0
|
|
50
|
+
|
|
51
|
+
messages = sanitized_payload.get("messages", [])
|
|
52
|
+
new_messages = []
|
|
53
|
+
|
|
54
|
+
for m in messages:
|
|
55
|
+
m_copy = dict(m)
|
|
56
|
+
content = m_copy.get("content", "")
|
|
57
|
+
if isinstance(content, str):
|
|
58
|
+
s_text, t_map, count = cls.sanitize_text(content)
|
|
59
|
+
m_copy["content"] = s_text
|
|
60
|
+
master_token_map.update(t_map)
|
|
61
|
+
total_scrubbed += count
|
|
62
|
+
new_messages.append(m_copy)
|
|
63
|
+
|
|
64
|
+
sanitized_payload["messages"] = new_messages
|
|
65
|
+
return sanitized_payload, master_token_map, total_scrubbed
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def rehydrate_response(cls, response_payload: Dict[str, Any], token_map: Dict[str, str]) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Restores original sensitive values into the assistant response text.
|
|
71
|
+
"""
|
|
72
|
+
if not token_map:
|
|
73
|
+
return response_payload
|
|
74
|
+
|
|
75
|
+
resp_copy = dict(response_payload)
|
|
76
|
+
choices = resp_copy.get("choices", [])
|
|
77
|
+
for c in choices:
|
|
78
|
+
msg = c.get("message", {})
|
|
79
|
+
if "content" in msg and isinstance(msg["content"], str):
|
|
80
|
+
for token, original in token_map.items():
|
|
81
|
+
msg["content"] = msg["content"].replace(token, original)
|
|
82
|
+
|
|
83
|
+
# Anthropic format
|
|
84
|
+
if "content" in resp_copy and isinstance(resp_copy["content"], list):
|
|
85
|
+
for block in resp_copy["content"]:
|
|
86
|
+
if isinstance(block, dict) and "text" in block:
|
|
87
|
+
for token, original in token_map.items():
|
|
88
|
+
block["text"] = block["text"].replace(token, original)
|
|
89
|
+
|
|
90
|
+
return resp_copy
|
|
91
|
+
|
|
92
|
+
# Global Privacy Shield instance
|
|
93
|
+
privacy_shield = PrivacyShield()
|
core/radix_tree.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Radix Prefix-Tree Engine for Multi-Turn AI Agent Dialogues.
|
|
3
|
+
Enables conversation branching, prefix sub-tree reuse, and 1024-token ephemeral cache alignment.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
class RadixNode:
|
|
12
|
+
"""A single conversation turn node in the Radix prefix tree."""
|
|
13
|
+
def __init__(self, node_id: str, role: str, content_hash: str, turn_index: int):
|
|
14
|
+
self.node_id = node_id
|
|
15
|
+
self.role = role
|
|
16
|
+
self.content_hash = content_hash
|
|
17
|
+
self.turn_index = turn_index
|
|
18
|
+
self.children: Dict[str, "RadixNode"] = {} # child_content_hash -> RadixNode
|
|
19
|
+
self.cached_completion: Optional[Dict[str, Any]] = None
|
|
20
|
+
self.tool_calls: Optional[List[Dict[str, Any]]] = None
|
|
21
|
+
self.created_at = time.time()
|
|
22
|
+
self.access_count = 0
|
|
23
|
+
self.last_accessed = time.time()
|
|
24
|
+
|
|
25
|
+
class RadixPrefixTree:
|
|
26
|
+
"""In-memory Radix Prefix Tree for multi-turn conversations and agent loops."""
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.root = RadixNode(node_id="root", role="system", content_hash="root", turn_index=-1)
|
|
29
|
+
self.total_nodes = 1
|
|
30
|
+
self.prefix_hits = 0
|
|
31
|
+
self.exact_hits = 0
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def hash_turn(turn: Dict[str, Any]) -> str:
|
|
35
|
+
"""Computes a deterministic hash of a single message turn."""
|
|
36
|
+
role = turn.get("role", "")
|
|
37
|
+
content = turn.get("content", "")
|
|
38
|
+
tool_calls = turn.get("tool_calls", None)
|
|
39
|
+
raw_repr = f"{role}:{content}:{json.dumps(tool_calls, sort_keys=True)}"
|
|
40
|
+
return hashlib.sha256(raw_repr.encode("utf-8")).hexdigest()[:16]
|
|
41
|
+
|
|
42
|
+
def match_prefix(self, messages: List[Dict[str, Any]]) -> Tuple[int, Optional[RadixNode]]:
|
|
43
|
+
"""
|
|
44
|
+
Traverses the tree to find the longest matching prefix of message turns.
|
|
45
|
+
Returns (matched_turn_count, last_matched_node).
|
|
46
|
+
"""
|
|
47
|
+
curr = self.root
|
|
48
|
+
matched_turns = 0
|
|
49
|
+
|
|
50
|
+
for i, turn in enumerate(messages):
|
|
51
|
+
turn_hash = self.hash_turn(turn)
|
|
52
|
+
if turn_hash in curr.children:
|
|
53
|
+
curr = curr.children[turn_hash]
|
|
54
|
+
curr.access_count += 1
|
|
55
|
+
curr.last_accessed = time.time()
|
|
56
|
+
matched_turns += 1
|
|
57
|
+
else:
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if matched_turns > 0:
|
|
61
|
+
self.prefix_hits += 1
|
|
62
|
+
|
|
63
|
+
return matched_turns, (curr if curr is not self.root else None)
|
|
64
|
+
|
|
65
|
+
def insert_conversation(self, messages: List[Dict[str, Any]], completion: Dict[str, Any], tool_calls: Optional[List[Dict[str, Any]]] = None) -> RadixNode:
|
|
66
|
+
"""
|
|
67
|
+
Inserts a full conversation path into the radix tree and stores the terminal completion.
|
|
68
|
+
"""
|
|
69
|
+
curr = self.root
|
|
70
|
+
for i, turn in enumerate(messages):
|
|
71
|
+
turn_hash = self.hash_turn(turn)
|
|
72
|
+
if turn_hash not in curr.children:
|
|
73
|
+
new_node_id = f"node_{self.total_nodes}_{turn_hash[:8]}"
|
|
74
|
+
new_node = RadixNode(
|
|
75
|
+
node_id=new_node_id,
|
|
76
|
+
role=turn.get("role", "user"),
|
|
77
|
+
content_hash=turn_hash,
|
|
78
|
+
turn_index=i
|
|
79
|
+
)
|
|
80
|
+
curr.children[turn_hash] = new_node
|
|
81
|
+
self.total_nodes += 1
|
|
82
|
+
curr = curr.children[turn_hash]
|
|
83
|
+
|
|
84
|
+
curr.cached_completion = completion
|
|
85
|
+
curr.tool_calls = tool_calls
|
|
86
|
+
curr.access_count += 1
|
|
87
|
+
curr.last_accessed = time.time()
|
|
88
|
+
return curr
|
|
89
|
+
|
|
90
|
+
def align_ephemeral_cache_blocks(self, messages: List[Dict[str, Any]], block_size_tokens: int = 1024) -> List[Dict[str, Any]]:
|
|
91
|
+
"""
|
|
92
|
+
Aligns message turns to downstream provider (Anthropic/OpenAI) 1024-token prompt caching blocks.
|
|
93
|
+
Injects Anthropic cache_control metadata on the last turn that crosses the 1024-token boundary.
|
|
94
|
+
"""
|
|
95
|
+
cumulative_tokens = 0
|
|
96
|
+
aligned_messages = []
|
|
97
|
+
|
|
98
|
+
for turn in messages:
|
|
99
|
+
turn_copy = dict(turn)
|
|
100
|
+
# Estimate token count ~ words * 1.3
|
|
101
|
+
content_str = str(turn.get("content", ""))
|
|
102
|
+
est_tokens = int(len(content_str.split()) * 1.3) + 4
|
|
103
|
+
cumulative_tokens += est_tokens
|
|
104
|
+
|
|
105
|
+
if cumulative_tokens >= block_size_tokens and "cache_control" not in turn_copy:
|
|
106
|
+
# Add ephemeral cache breakpoint
|
|
107
|
+
turn_copy["cache_control"] = {"type": "ephemeral"}
|
|
108
|
+
cumulative_tokens = 0 # reset for next block
|
|
109
|
+
|
|
110
|
+
aligned_messages.append(turn_copy)
|
|
111
|
+
|
|
112
|
+
return aligned_messages
|
|
113
|
+
|
|
114
|
+
# Global Radix Prefix Tree instance
|
|
115
|
+
radix_tree = RadixPrefixTree()
|