guardian-runtime 0.1.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.
- guardian/__init__.py +51 -0
- guardian/cli/__init__.py +0 -0
- guardian/cli/main.py +17 -0
- guardian/core/__init__.py +0 -0
- guardian/core/engine.py +19 -0
- guardian/core/license.py +21 -0
- guardian/core/policy.py +290 -0
- guardian/core/storage.py +27 -0
- guardian/finops/__init__.py +0 -0
- guardian/finops/cost_calculator.py +29 -0
- guardian/finops/token_counter.py +14 -0
- guardian/guards/__init__.py +0 -0
- guardian/guards/validators/__init__.py +0 -0
- guardian/guards/validators/hallucination.py +31 -0
- guardian/guards/validators/jailbreak.py +33 -0
- guardian/guards/validators/pii.py +369 -0
- guardian_runtime-0.1.0.dist-info/METADATA +622 -0
- guardian_runtime-0.1.0.dist-info/RECORD +20 -0
- guardian_runtime-0.1.0.dist-info/WHEEL +4 -0
- guardian_runtime-0.1.0.dist-info/entry_points.txt +2 -0
guardian/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Guardian Runtime — Local-first AI governance layer.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from guardian import Guardian
|
|
6
|
+
|
|
7
|
+
guardian = Guardian.from_policy("policy.yaml")
|
|
8
|
+
response = guardian.complete(model="gpt-4", messages=[...])
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
11
|
+
|
|
12
|
+
from guardian.core.engine import GuardianEngine
|
|
13
|
+
from guardian.core.policy import load_policy
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__all__ = ["Guardian"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Guardian:
|
|
20
|
+
"""Main entry point for Guardian Runtime."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, engine: GuardianEngine):
|
|
23
|
+
self._engine = engine
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_policy(cls, path: str) -> "Guardian":
|
|
27
|
+
"""Load a Guardian instance from a YAML policy file."""
|
|
28
|
+
policy = load_policy(path)
|
|
29
|
+
engine = GuardianEngine(policy)
|
|
30
|
+
return cls(engine)
|
|
31
|
+
|
|
32
|
+
def complete(
|
|
33
|
+
self,
|
|
34
|
+
model: str,
|
|
35
|
+
messages: list,
|
|
36
|
+
agent_id: str = "default",
|
|
37
|
+
session_id: str | None = None,
|
|
38
|
+
**kwargs,
|
|
39
|
+
):
|
|
40
|
+
"""Wrap an LLM call with full Guardian governance."""
|
|
41
|
+
return self._engine.complete(
|
|
42
|
+
model=model,
|
|
43
|
+
messages=messages,
|
|
44
|
+
agent_id=agent_id,
|
|
45
|
+
session_id=session_id,
|
|
46
|
+
**kwargs,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def get_cost_report(self, agent_id: str = "default") -> dict:
|
|
50
|
+
"""Return cost report for the given agent."""
|
|
51
|
+
return self._engine.get_cost_report(agent_id)
|
guardian/cli/__init__.py
ADDED
|
File without changes
|
guardian/cli/main.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Guardian CLI — entry point for all `guardian` commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
@click.version_option(package_name="guardian-runtime")
|
|
8
|
+
def cli():
|
|
9
|
+
"""⛨ Guardian Runtime — Local-first AI governance."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Commands will be registered here as they are built
|
|
14
|
+
# from guardian.cli.init import init_command
|
|
15
|
+
# from guardian.cli.validate import validate_command
|
|
16
|
+
# from guardian.cli.status import status_command
|
|
17
|
+
# from guardian.cli.logs import logs_command
|
|
File without changes
|
guardian/core/engine.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Guardian core engine — orchestrates the full request/response pipeline."""
|
|
2
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
3
|
+
# TODO (Week 6): Implement GuardianEngine.complete() pipeline
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GuardianEngine:
|
|
7
|
+
"""Orchestrates: license check → input guard → LLM → output guard → log."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, policy):
|
|
10
|
+
self.policy = policy
|
|
11
|
+
# TODO: initialize sub-components from policy
|
|
12
|
+
|
|
13
|
+
def complete(self, model, messages, agent_id="default", session_id=None, **kwargs):
|
|
14
|
+
"""Full governed LLM call. Returns GuardianResponse."""
|
|
15
|
+
raise NotImplementedError("Engine not yet implemented — see ARCHITECTURE.md §4.2")
|
|
16
|
+
|
|
17
|
+
def get_cost_report(self, agent_id: str) -> dict:
|
|
18
|
+
"""Return local cost report for the given agent."""
|
|
19
|
+
raise NotImplementedError
|
guardian/core/license.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""License key validation and once-daily server sync."""
|
|
2
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
3
|
+
# TODO (Week 6): Implement LicenseManager
|
|
4
|
+
# See ARCHITECTURE.md §4.9 for full spec — especially fail-open and grace period logic
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LicenseManager:
|
|
8
|
+
"""Validates license locally and syncs with guardian-ai.dev once per day."""
|
|
9
|
+
|
|
10
|
+
LICENSE_SERVER_URL = "https://guardian-ai.dev/api/validate"
|
|
11
|
+
|
|
12
|
+
def check_or_sync(self):
|
|
13
|
+
"""Called before every guardian.complete(). Syncs if >24h since last sync."""
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
def sync_with_server(self):
|
|
17
|
+
"""POST { license_key, checks_used } → receive { valid, plan, limit, expiry }."""
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
def is_initialized(self) -> bool:
|
|
21
|
+
raise NotImplementedError
|
guardian/core/policy.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""YAML policy loader and Pydantic schema validation.
|
|
2
|
+
|
|
3
|
+
Reads a guardian_policy.yaml file, validates it against a strict Pydantic V2
|
|
4
|
+
schema, and returns a Policy object that every Guardian component uses.
|
|
5
|
+
|
|
6
|
+
If the YAML has typos, missing fields, or wrong types, Pydantic will raise a
|
|
7
|
+
clear PolicyValidationError with details.
|
|
8
|
+
|
|
9
|
+
See ARCHITECTURE.md §4.7 for full specification.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Enums for strict validation
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
class PIIAction(str, Enum):
|
|
26
|
+
"""Action to take when PII is detected."""
|
|
27
|
+
BLOCK = "block"
|
|
28
|
+
REDACT = "redact"
|
|
29
|
+
FLAG = "flag"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LogSink(str, Enum):
|
|
33
|
+
"""Where to write Guardian logs."""
|
|
34
|
+
JSONL = "jsonl"
|
|
35
|
+
CONSOLE = "console"
|
|
36
|
+
BOTH = "both"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LogLevel(str, Enum):
|
|
40
|
+
"""What severity of events to log."""
|
|
41
|
+
ALL = "ALL"
|
|
42
|
+
VIOLATIONS_ONLY = "VIOLATIONS_ONLY"
|
|
43
|
+
HIGH_SEVERITY = "HIGH_SEVERITY"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HallucinationProvider(str, Enum):
|
|
47
|
+
"""Supported LLM providers for hallucination detection (BYOM)."""
|
|
48
|
+
OPENAI = "openai"
|
|
49
|
+
ANTHROPIC = "anthropic"
|
|
50
|
+
OLLAMA = "ollama"
|
|
51
|
+
GEMINI = "gemini"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LoopAction(str, Enum):
|
|
55
|
+
"""Action to take when a prompt loop is detected."""
|
|
56
|
+
BLOCK = "block"
|
|
57
|
+
BLOCK_AND_ALERT = "block_and_alert"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Valid PII entity names — must match PIIType enum values in pii.py
|
|
61
|
+
VALID_PII_ENTITIES = frozenset({
|
|
62
|
+
"ssn", "credit_card", "email", "phone",
|
|
63
|
+
"aadhaar", "pan", "upi_id", "passport", "secret",
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Exceptions
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
class PolicyValidationError(Exception):
|
|
72
|
+
"""Raised when a YAML policy file fails schema validation.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
errors: List of Pydantic validation error dicts.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, message: str, errors: Optional[List[Dict[str, Any]]] = None):
|
|
79
|
+
super().__init__(message)
|
|
80
|
+
self.errors = errors or []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Config sub-models
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
class ScopeConfig(BaseModel):
|
|
88
|
+
"""Topic-scoping configuration for the Input Guard."""
|
|
89
|
+
allowed_topics: List[str] = Field(default_factory=list)
|
|
90
|
+
block_message: str = "This topic is outside my scope."
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class InputGuardConfig(BaseModel):
|
|
94
|
+
"""Configuration for the Input Guard pipeline."""
|
|
95
|
+
pii_detection: bool = True
|
|
96
|
+
pii_entities: List[str] = Field(
|
|
97
|
+
default_factory=lambda: list(VALID_PII_ENTITIES),
|
|
98
|
+
description="List of PII entity types to detect.",
|
|
99
|
+
)
|
|
100
|
+
pii_action: PIIAction = PIIAction.BLOCK
|
|
101
|
+
jailbreak_detection: bool = True
|
|
102
|
+
scope: Optional[ScopeConfig] = None
|
|
103
|
+
|
|
104
|
+
@field_validator("pii_entities")
|
|
105
|
+
@classmethod
|
|
106
|
+
def validate_pii_entities(cls, v: List[str]) -> List[str]:
|
|
107
|
+
"""Ensure every entity name matches a known PIIType value."""
|
|
108
|
+
invalid = [e for e in v if e not in VALID_PII_ENTITIES]
|
|
109
|
+
if invalid:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"Unknown PII entities: {invalid}. "
|
|
112
|
+
f"Valid values: {sorted(VALID_PII_ENTITIES)}"
|
|
113
|
+
)
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class OutputGuardConfig(BaseModel):
|
|
118
|
+
"""Configuration for the Output Guard pipeline."""
|
|
119
|
+
hallucination_check: bool = False
|
|
120
|
+
hallucination_provider: HallucinationProvider = HallucinationProvider.OPENAI
|
|
121
|
+
hallucination_model: str = "gpt-4o-mini"
|
|
122
|
+
pii_detection: bool = True
|
|
123
|
+
profanity_filter: bool = False
|
|
124
|
+
competitor_block: List[str] = Field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AutoDowngradeConfig(BaseModel):
|
|
128
|
+
"""Auto-downgrade model when budget threshold is reached."""
|
|
129
|
+
enabled: bool = False
|
|
130
|
+
threshold: float = Field(default=0.80, ge=0.0, le=1.0)
|
|
131
|
+
target_model: str = "gpt-3.5-turbo"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class LoopConfig(BaseModel):
|
|
135
|
+
"""Semantic loop detection settings."""
|
|
136
|
+
max_retries: int = Field(default=3, ge=1)
|
|
137
|
+
similarity_threshold: float = Field(default=0.90, ge=0.0, le=1.0)
|
|
138
|
+
action: LoopAction = LoopAction.BLOCK_AND_ALERT
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CostConfig(BaseModel):
|
|
142
|
+
"""FinOps budget and cost control settings."""
|
|
143
|
+
daily_budget: float = Field(default=10.00, ge=0.0)
|
|
144
|
+
monthly_budget: Optional[float] = Field(default=None, ge=0.0)
|
|
145
|
+
per_session_limit: float = Field(default=0.50, ge=0.0)
|
|
146
|
+
currency: str = "USD"
|
|
147
|
+
auto_downgrade: Optional[AutoDowngradeConfig] = None
|
|
148
|
+
loop_detection: Optional[LoopConfig] = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class RateLimitConfig(BaseModel):
|
|
152
|
+
"""Rate limit for a specific tool."""
|
|
153
|
+
max_calls: int = Field(ge=1)
|
|
154
|
+
per: str = "session" # "session" | "minute" | "hour"
|
|
155
|
+
cooldown_seconds: int = Field(default=0, ge=0)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ArgRuleConfig(BaseModel):
|
|
159
|
+
"""Argument validation rule for a tool parameter."""
|
|
160
|
+
type: str = "string" # "string" | "int" | "enum"
|
|
161
|
+
pattern: Optional[str] = None
|
|
162
|
+
values: Optional[List[str]] = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ToolConfig(BaseModel):
|
|
166
|
+
"""Tool governance configuration."""
|
|
167
|
+
allowed: List[str] = Field(default_factory=list)
|
|
168
|
+
denied: List[str] = Field(default_factory=list)
|
|
169
|
+
rate_limits: Dict[str, RateLimitConfig] = Field(default_factory=dict)
|
|
170
|
+
argument_validation: Dict[str, Dict[str, ArgRuleConfig]] = Field(
|
|
171
|
+
default_factory=dict
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class LoggingConfig(BaseModel):
|
|
176
|
+
"""Logging configuration."""
|
|
177
|
+
sink: LogSink = LogSink.JSONL
|
|
178
|
+
log_level: LogLevel = LogLevel.ALL
|
|
179
|
+
retention_days: int = Field(default=30, ge=1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class AlertConfig(BaseModel):
|
|
183
|
+
"""Alert/notification configuration (v0.2+)."""
|
|
184
|
+
slack_webhook: Optional[str] = None
|
|
185
|
+
email: Optional[str] = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ComplianceConfig(BaseModel):
|
|
189
|
+
"""Compliance framework configuration."""
|
|
190
|
+
frameworks: List[str] = Field(default_factory=list) # e.g. ["dpdp", "gdpr"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# Agent-level policy
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
class AgentPolicy(BaseModel):
|
|
198
|
+
"""Complete policy for a single agent."""
|
|
199
|
+
input_guard: Optional[InputGuardConfig] = None
|
|
200
|
+
output_guard: Optional[OutputGuardConfig] = None
|
|
201
|
+
cost: Optional[CostConfig] = None
|
|
202
|
+
tools: Optional[ToolConfig] = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Root Policy model
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
class Policy(BaseModel):
|
|
210
|
+
"""Root policy model — represents a complete guardian_policy.yaml file.
|
|
211
|
+
|
|
212
|
+
Usage:
|
|
213
|
+
policy = load_policy("guardian_policy.yaml")
|
|
214
|
+
agent_config = policy.get_agent("support-bot")
|
|
215
|
+
"""
|
|
216
|
+
version: str = "1.0"
|
|
217
|
+
name: str = "default"
|
|
218
|
+
environment: Optional[str] = None # "dev" | "staging" | "production"
|
|
219
|
+
agents: Dict[str, AgentPolicy] = Field(default_factory=dict)
|
|
220
|
+
logging: Optional[LoggingConfig] = None
|
|
221
|
+
alerts: Optional[AlertConfig] = None
|
|
222
|
+
compliance: Optional[ComplianceConfig] = None
|
|
223
|
+
|
|
224
|
+
@model_validator(mode="after")
|
|
225
|
+
def ensure_default_agent(self) -> "Policy":
|
|
226
|
+
"""Ensure there is at least a 'default' agent entry."""
|
|
227
|
+
if not self.agents:
|
|
228
|
+
self.agents = {"default": AgentPolicy()}
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def get_agent(self, agent_id: str) -> AgentPolicy:
|
|
232
|
+
"""Get configuration for a specific agent, falling back to 'default'.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
agent_id: The agent identifier to look up.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
AgentPolicy for the given agent, or the 'default' agent if not found.
|
|
239
|
+
"""
|
|
240
|
+
if agent_id in self.agents:
|
|
241
|
+
return self.agents[agent_id]
|
|
242
|
+
return self.agents.get("default", AgentPolicy())
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# Public API
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def load_policy(path: str | Path) -> Policy:
|
|
250
|
+
"""Load and validate a Guardian YAML policy file.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
path: Path to the YAML policy file.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
A fully validated Policy object.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
FileNotFoundError: If the YAML file does not exist.
|
|
260
|
+
PolicyValidationError: If the YAML content fails schema validation.
|
|
261
|
+
"""
|
|
262
|
+
path = Path(path)
|
|
263
|
+
|
|
264
|
+
if not path.exists():
|
|
265
|
+
raise FileNotFoundError(f"Policy file not found: {path}")
|
|
266
|
+
|
|
267
|
+
with open(path, "r") as f:
|
|
268
|
+
raw = yaml.safe_load(f)
|
|
269
|
+
|
|
270
|
+
if raw is None:
|
|
271
|
+
raise PolicyValidationError(
|
|
272
|
+
f"Policy file is empty: {path}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if not isinstance(raw, dict):
|
|
276
|
+
raise PolicyValidationError(
|
|
277
|
+
f"Policy file must contain a YAML mapping, got {type(raw).__name__}: {path}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
return Policy.model_validate(raw)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
# Re-wrap Pydantic ValidationError into our own exception
|
|
284
|
+
errors = []
|
|
285
|
+
if hasattr(e, "errors"):
|
|
286
|
+
errors = e.errors() # type: ignore[union-attr]
|
|
287
|
+
raise PolicyValidationError(
|
|
288
|
+
f"Policy validation failed for {path}: {e}",
|
|
289
|
+
errors=errors,
|
|
290
|
+
) from e
|
guardian/core/storage.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Local ~/.guardian/ file manager — config.json and usage.json."""
|
|
2
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
3
|
+
# TODO (Week 6): Implement LocalStorage
|
|
4
|
+
# See ARCHITECTURE.md §4.8 for full spec
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LocalStorage:
|
|
8
|
+
"""Manages ~/.guardian/config.json and ~/.guardian/usage.json."""
|
|
9
|
+
|
|
10
|
+
def save_license(self, key: str, plan: str, limit: int, expiry: str | None = None):
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
def load_license(self) -> dict | None:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
def increment_usage(self) -> int:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def get_usage(self) -> dict:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
def check_usage_limit(self) -> tuple[bool, int, int]:
|
|
23
|
+
"""Returns (within_limit, current_count, plan_limit)."""
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def mark_synced(self, timestamp: str):
|
|
27
|
+
raise NotImplementedError
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Per-model cost tables and cost estimation."""
|
|
2
|
+
# TODO (Week 4): Implement estimate_cost()
|
|
3
|
+
# See ARCHITECTURE.md §4.5 — keep this table updated as providers change pricing
|
|
4
|
+
|
|
5
|
+
# Cost per 1,000 tokens in USD
|
|
6
|
+
MODEL_COST_PER_1K: dict[str, dict[str, float]] = {
|
|
7
|
+
# OpenAI
|
|
8
|
+
"gpt-4o": {"input": 0.005, "output": 0.015},
|
|
9
|
+
"gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
|
|
10
|
+
"gpt-4-turbo": {"input": 0.01, "output": 0.03},
|
|
11
|
+
"gpt-4": {"input": 0.03, "output": 0.06},
|
|
12
|
+
"gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
|
|
13
|
+
# Anthropic
|
|
14
|
+
"claude-3-5-sonnet": {"input": 0.003, "output": 0.015},
|
|
15
|
+
"claude-3-opus": {"input": 0.015, "output": 0.075},
|
|
16
|
+
"claude-3-haiku": {"input": 0.00025, "output": 0.00125},
|
|
17
|
+
# Google
|
|
18
|
+
"gemini-1-5-pro": {"input": 0.00125, "output": 0.005},
|
|
19
|
+
"gemini-1-5-flash": {"input": 0.000075,"output": 0.0003},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def estimate_cost(input_tokens: int, output_tokens: int = 0, model: str = "gpt-4o") -> float:
|
|
24
|
+
"""Estimate cost in USD for given token counts and model."""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_supported_models() -> list[str]:
|
|
29
|
+
return list(MODEL_COST_PER_1K.keys())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Token counting — tiktoken wrapper."""
|
|
2
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
3
|
+
# TODO (Week 4): Implement token counting functions
|
|
4
|
+
# See ARCHITECTURE.md §4.5
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def count_tokens(text: str, model: str = "gpt-4") -> int:
|
|
8
|
+
"""Count tokens in text for the given model using tiktoken."""
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def count_messages_tokens(messages: list[dict], model: str = "gpt-4") -> int:
|
|
13
|
+
"""Count total tokens for a list of chat messages including ChatML overhead."""
|
|
14
|
+
raise NotImplementedError
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Hallucination detector — LLM-as-judge pattern."""
|
|
2
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
3
|
+
# TODO (Week 5): Implement HallucinationDetector
|
|
4
|
+
# See ARCHITECTURE.md §4.4.1
|
|
5
|
+
# Note: uses developer's OpenAI API key for the judge call, NOT ours
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HallucinationResult:
|
|
12
|
+
verdict: str # "grounded" | "partially_grounded" | "hallucinated"
|
|
13
|
+
confidence: float
|
|
14
|
+
unsupported_claims: list[str] = field(default_factory=list)
|
|
15
|
+
explanation: str = ""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def is_hallucination(self) -> bool:
|
|
19
|
+
return self.verdict == "hallucinated"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HallucinationDetector:
|
|
23
|
+
"""Uses a small LLM (gpt-4o-mini) to verify response grounding in context."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, judge_model: str = "gpt-4o-mini", threshold: float = 0.7):
|
|
26
|
+
self.judge_model = judge_model
|
|
27
|
+
self.threshold = threshold
|
|
28
|
+
|
|
29
|
+
def check(self, question: str, response: str, context: str) -> HallucinationResult:
|
|
30
|
+
"""Judge whether the response is grounded in the provided context."""
|
|
31
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Jailbreak and prompt injection detector — 50+ patterns, zero external deps."""
|
|
2
|
+
from __future__ import annotations # Python 3.9 compatibility
|
|
3
|
+
# TODO (Week 4): Implement JailbreakDetector
|
|
4
|
+
# See ARCHITECTURE.md §4.3.2
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class JailbreakResult:
|
|
12
|
+
is_jailbreak: bool
|
|
13
|
+
confidence: float
|
|
14
|
+
pattern_matched: str | None
|
|
15
|
+
category: str | None # "dan" | "instruction_override" | "role_play" | "encoding" | "extraction"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# TODO (Week 4): Fill in all 50+ patterns from ARCHITECTURE.md §4.3.2
|
|
19
|
+
JAILBREAK_PATTERNS: list[tuple[str, str]] = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JailbreakDetector:
|
|
23
|
+
"""Detects jailbreak attempts using compiled regex patterns."""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self._compiled = [
|
|
27
|
+
(re.compile(pattern, re.IGNORECASE), category)
|
|
28
|
+
for pattern, category in JAILBREAK_PATTERNS
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def detect(self, text: str) -> JailbreakResult:
|
|
32
|
+
"""Return first matching jailbreak pattern, or is_jailbreak=False."""
|
|
33
|
+
raise NotImplementedError
|