logitgate 0.2.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.
logitgate/__init__.py ADDED
@@ -0,0 +1,103 @@
1
+ from logitgate.backends.base import (
2
+ GenerativeBackend,
3
+ LogitBackend,
4
+ StubBackend,
5
+ infer_backend_spec,
6
+ resolve_generative_backend,
7
+ resolve_logit_backend,
8
+ )
9
+ from logitgate.backends.llamacpp import LlamaCppBackend
10
+ from logitgate.backends.ollama import OllamaBackend
11
+ from logitgate.backends.openai_compat import OpenAICompatBackend
12
+ from logitgate.backends.recurrent import RecurrentPyTorchBackend
13
+ from logitgate.cascade import Cascade
14
+ from logitgate.config import (
15
+ LogitGateDefaults,
16
+ configure,
17
+ get_default_backend,
18
+ get_default_save_failures,
19
+ get_default_snapshot_dir,
20
+ reset_config,
21
+ )
22
+ from logitgate.debiasing.inversion import (
23
+ compile_batch_binary_prompt,
24
+ compile_binary_prompt,
25
+ compile_choice_prompt,
26
+ compile_score_prompt,
27
+ )
28
+ from logitgate.debiasing.permutation import (
29
+ KNOWN_MODEL_TEMPERATURES,
30
+ CalibrationResult,
31
+ auto_calibrate_backend,
32
+ combine_symmetric_log_odds,
33
+ fit_calibration,
34
+ get_model_temperature,
35
+ )
36
+ from logitgate.debiasing.softmax import (
37
+ compute_binary_log_odds,
38
+ compute_normalized_probabilities,
39
+ extract_token_logprob,
40
+ )
41
+ from logitgate.gates.ast_gate import ASTGate, ContractGate
42
+ from logitgate.gates.base import BaseGate
43
+ from logitgate.gates.judge_gate import JudgeGate
44
+ from logitgate.gates.logit_gate import ChoiceGate, LogitGate, ScoreGate
45
+ from logitgate.integrations.deepeval import LogitGateMetric, LogitGateSystemOneModel
46
+ from logitgate.integrations.langgraph import as_conditional_edge
47
+ from logitgate.integrations.pytest_plugin import check
48
+ from logitgate.models import (
49
+ DecisionStatus,
50
+ LayerTrace,
51
+ Verdict,
52
+ )
53
+ from logitgate.snapshot import VerdictSnapshot
54
+
55
+ __version__ = "0.2.0"
56
+
57
+ __all__ = [
58
+ "KNOWN_MODEL_TEMPERATURES",
59
+ "ASTGate",
60
+ "BaseGate",
61
+ "CalibrationResult",
62
+ "Cascade",
63
+ "ChoiceGate",
64
+ "ContractGate",
65
+ "DecisionStatus",
66
+ "GenerativeBackend",
67
+ "JudgeGate",
68
+ "LayerTrace",
69
+ "LlamaCppBackend",
70
+ "LogitBackend",
71
+ "LogitGate",
72
+ "LogitGateDefaults",
73
+ "LogitGateMetric",
74
+ "LogitGateSystemOneModel",
75
+ "OllamaBackend",
76
+ "OpenAICompatBackend",
77
+ "RecurrentPyTorchBackend",
78
+ "ScoreGate",
79
+ "StubBackend",
80
+ "Verdict",
81
+ "VerdictSnapshot",
82
+ "as_conditional_edge",
83
+ "auto_calibrate_backend",
84
+ "check",
85
+ "combine_symmetric_log_odds",
86
+ "compile_batch_binary_prompt",
87
+ "compile_binary_prompt",
88
+ "compile_choice_prompt",
89
+ "compile_score_prompt",
90
+ "compute_binary_log_odds",
91
+ "compute_normalized_probabilities",
92
+ "configure",
93
+ "extract_token_logprob",
94
+ "fit_calibration",
95
+ "get_default_backend",
96
+ "get_default_save_failures",
97
+ "get_default_snapshot_dir",
98
+ "get_model_temperature",
99
+ "infer_backend_spec",
100
+ "reset_config",
101
+ "resolve_generative_backend",
102
+ "resolve_logit_backend",
103
+ ]
@@ -0,0 +1,25 @@
1
+ from logitgate.backends.base import (
2
+ GenerativeBackend,
3
+ LogitBackend,
4
+ StubBackend,
5
+ infer_backend_spec,
6
+ resolve_generative_backend,
7
+ resolve_logit_backend,
8
+ )
9
+ from logitgate.backends.llamacpp import LlamaCppBackend
10
+ from logitgate.backends.ollama import OllamaBackend
11
+ from logitgate.backends.openai_compat import OpenAICompatBackend
12
+ from logitgate.backends.recurrent import RecurrentPyTorchBackend
13
+
14
+ __all__ = [
15
+ "GenerativeBackend",
16
+ "LlamaCppBackend",
17
+ "LogitBackend",
18
+ "OllamaBackend",
19
+ "OpenAICompatBackend",
20
+ "RecurrentPyTorchBackend",
21
+ "StubBackend",
22
+ "infer_backend_spec",
23
+ "resolve_generative_backend",
24
+ "resolve_logit_backend",
25
+ ]
@@ -0,0 +1,259 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import asyncio
5
+ import math
6
+ import re
7
+ from typing import Any
8
+ from urllib.parse import urlparse
9
+
10
+
11
+ def strip_reasoning_blocks(text: str) -> str:
12
+ """Strips <think>...</think> reasoning traces from model responses."""
13
+ cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
14
+ if "<think>" in cleaned:
15
+ cleaned = re.sub(r"<think>.*$", "", cleaned, flags=re.DOTALL)
16
+ return cleaned.strip()
17
+
18
+
19
+ class LogitBackend(abc.ABC):
20
+ """Abstract base class for extracting first-token log-probabilities."""
21
+
22
+ @abc.abstractmethod
23
+ def query_top_logprobs(
24
+ self,
25
+ prompt: str,
26
+ top_k: int = 10,
27
+ allowed_tokens: list[str] | None = None,
28
+ ) -> list[dict[str, Any]]:
29
+ """Returns top_k token logprobs: [{'token': str, 'logprob': float}, ...]"""
30
+
31
+ async def aquery_top_logprobs(
32
+ self,
33
+ prompt: str,
34
+ top_k: int = 10,
35
+ allowed_tokens: list[str] | None = None,
36
+ ) -> list[dict[str, Any]]:
37
+ """Async variant of query_top_logprobs."""
38
+ loop = asyncio.get_running_loop()
39
+ return await loop.run_in_executor(
40
+ None, self.query_top_logprobs, prompt, top_k, allowed_tokens
41
+ )
42
+
43
+
44
+ class GenerativeBackend(abc.ABC):
45
+ """Abstract base class for generative reasoning & diagnostics."""
46
+
47
+ @abc.abstractmethod
48
+ def generate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
49
+ """Returns (generated_text, tokens_generated)."""
50
+
51
+ async def agenerate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
52
+ """Async variant of generate."""
53
+ loop = asyncio.get_running_loop()
54
+ return await loop.run_in_executor(None, lambda: self.generate(prompt, max_tokens=max_tokens, **kwargs))
55
+
56
+
57
+ class StubBackend(LogitBackend, GenerativeBackend):
58
+ """
59
+ Offline deterministic stub backend for sub-millisecond unit tests and CI.
60
+ Records all prompted inputs in self.prompt_log.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ default_prob: float = 0.95,
66
+ forward_top_logprobs: list[dict[str, Any]] | None = None,
67
+ reverse_top_logprobs: list[dict[str, Any]] | None = None,
68
+ logprob_responses: list[list[dict[str, Any]]] | None = None,
69
+ diagnostic_text: str = "[VERDICT: PASS] All criteria met.",
70
+ tokens_generated: int = 0,
71
+ ) -> None:
72
+ self.default_prob = default_prob
73
+ self.forward_top_logprobs = forward_top_logprobs
74
+ self.reverse_top_logprobs = reverse_top_logprobs
75
+ self.logprob_responses = list(logprob_responses) if logprob_responses else None
76
+ self.diagnostic_text = diagnostic_text
77
+ self.tokens_generated = tokens_generated
78
+ self.prompt_log: list[str] = []
79
+ self._response_idx = 0
80
+
81
+ def __repr__(self) -> str:
82
+ return f"StubBackend(default_prob={self.default_prob})"
83
+
84
+ def query_top_logprobs(
85
+ self,
86
+ prompt: str,
87
+ top_k: int = 10,
88
+ allowed_tokens: list[str] | None = None,
89
+ ) -> list[dict[str, Any]]:
90
+ self.prompt_log.append(prompt)
91
+
92
+ # 1. Explicit queue response
93
+ if self.logprob_responses:
94
+ resp = self.logprob_responses[self._response_idx % len(self.logprob_responses)]
95
+ self._response_idx += 1
96
+ return resp
97
+
98
+ # 2. Check if this is Pass 2 (reversed mapping where A is Fail / violates)
99
+ is_reverse_pass = (
100
+ "A: The candidate artifact violates" in prompt
101
+ or "A: The content violates" in prompt
102
+ or "A: The candidate does not" in prompt
103
+ )
104
+
105
+ if is_reverse_pass:
106
+ if self.reverse_top_logprobs is not None:
107
+ return self.reverse_top_logprobs
108
+ p = max(min(self.default_prob, 0.9999), 0.0001)
109
+ return [
110
+ {"token": "B", "logprob": math.log(p)},
111
+ {"token": "A", "logprob": math.log(1.0 - p)},
112
+ ]
113
+ else:
114
+ if self.forward_top_logprobs is not None:
115
+ return self.forward_top_logprobs
116
+ p = max(min(self.default_prob, 0.9999), 0.0001)
117
+ return [
118
+ {"token": "A", "logprob": math.log(p)},
119
+ {"token": "B", "logprob": math.log(1.0 - p)},
120
+ ]
121
+
122
+ async def aquery_top_logprobs(
123
+ self,
124
+ prompt: str,
125
+ top_k: int = 10,
126
+ allowed_tokens: list[str] | None = None,
127
+ ) -> list[dict[str, Any]]:
128
+ return self.query_top_logprobs(prompt, top_k, allowed_tokens)
129
+
130
+ def generate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
131
+ self.prompt_log.append(prompt)
132
+ return self.diagnostic_text, self.tokens_generated
133
+
134
+ async def agenerate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
135
+ return self.generate(prompt, max_tokens=max_tokens, **kwargs)
136
+
137
+
138
+ def infer_backend_spec(backend: Any) -> str:
139
+ """Infers a canonical URI specifier string from a resolved backend instance."""
140
+ if isinstance(backend, str):
141
+ return backend
142
+ if isinstance(backend, StubBackend):
143
+ return f"stub://{backend.default_prob}" if backend.default_prob != 0.95 else "stub://default"
144
+ cls_name = backend.__class__.__name__
145
+ base_url = getattr(backend, "base_url", None) or ""
146
+ model = getattr(backend, "model", None) or ""
147
+ if cls_name == "OllamaBackend":
148
+ host = base_url.replace("http://", "").replace("https://", "").rstrip("/") or "localhost:11434"
149
+ return f"ollama://{host}/{model}" if model else f"ollama://{host}"
150
+ if cls_name == "OpenAICompatBackend":
151
+ host = base_url.replace("https://", "").replace("http://", "").rstrip("/")
152
+ scheme = "openai" if base_url.startswith("https://") else "vllm"
153
+ return f"{scheme}://{host}"
154
+ if cls_name == "LlamaCppBackend":
155
+ if base_url:
156
+ host = base_url.replace("http://", "").replace("https://", "").rstrip("/")
157
+ return f"llamacpp://{host}/{model}" if model else f"llamacpp://{host}"
158
+ return f"llamacpp:///{model}" if model else "llamacpp"
159
+ return "stub://default"
160
+
161
+
162
+ def resolve_logit_backend(
163
+ backend_spec: str | LogitBackend | None = None,
164
+ model: str | None = None,
165
+ **kwargs: Any,
166
+ ) -> LogitBackend:
167
+ if isinstance(backend_spec, LogitBackend):
168
+ return backend_spec
169
+
170
+ from logitgate.config import get_default_backend, get_default_model
171
+
172
+ explicit_model = model
173
+ if backend_spec is None or backend_spec == "default":
174
+ default_spec = get_default_backend()
175
+ if model is None:
176
+ model = get_default_model()
177
+ if (
178
+ default_spec == "stub://default"
179
+ and explicit_model is not None
180
+ and explicit_model not in ("default", "stub")
181
+ and not explicit_model.startswith("stub")
182
+ ):
183
+ backend_spec = f"ollama://localhost:11434/{explicit_model}"
184
+ else:
185
+ backend_spec = default_spec
186
+ elif model is None:
187
+ model = get_default_model()
188
+
189
+ spec = str(backend_spec).strip()
190
+ if spec.startswith("stub://") or spec == "stub" or spec == "stub://default":
191
+ prob = 0.95
192
+ if "/" in spec and spec not in ("stub://", "stub://default"):
193
+ part = spec.split("/")[-1]
194
+ try:
195
+ prob = float(part)
196
+ except ValueError:
197
+ pass
198
+ return StubBackend(default_prob=prob, **kwargs)
199
+
200
+ if spec.startswith("ollama://"):
201
+ from logitgate.backends.ollama import OllamaBackend
202
+ parsed = urlparse(spec)
203
+ netloc = parsed.netloc or "localhost:11434"
204
+ base_url = f"http://{netloc}"
205
+ parsed_model = parsed.path.lstrip("/") if parsed.path else None
206
+ effective_model = model or parsed_model or "qwen2.5-coder:14b"
207
+ return OllamaBackend(base_url=base_url, model=effective_model, **kwargs)
208
+
209
+ if spec.startswith(("openai://", "vllm://")):
210
+ from logitgate.backends.openai_compat import OpenAICompatBackend
211
+ parsed = urlparse(spec)
212
+ scheme = "https" if spec.startswith("openai://") else "http"
213
+ base_url = f"{scheme}://{parsed.netloc}{parsed.path}"
214
+ effective_model = model or "gpt-4o-mini"
215
+ return OpenAICompatBackend(base_url=base_url, model=effective_model, **kwargs)
216
+
217
+ if spec.startswith("llamacpp://") or spec == "llamacpp":
218
+ from logitgate.backends.llamacpp import LlamaCppBackend
219
+ if spec == "llamacpp":
220
+ return LlamaCppBackend(model=model, **kwargs)
221
+ parsed = urlparse(spec)
222
+ if parsed.netloc:
223
+ base_url = f"http://{parsed.netloc}"
224
+ parsed_model = parsed.path.lstrip("/") if parsed.path else None
225
+ effective_model_opt: str | None = model or parsed_model or None
226
+ return LlamaCppBackend(base_url=base_url, model=effective_model_opt, **kwargs)
227
+ else:
228
+ effective_model_opt = model or (parsed.path.lstrip("/") if parsed.path else None)
229
+ return LlamaCppBackend(model=effective_model_opt, **kwargs)
230
+
231
+ if spec.startswith("recurrent://") or spec == "recurrent":
232
+ from logitgate.backends.recurrent import RecurrentPyTorchBackend
233
+ if spec.startswith("recurrent://"):
234
+ parsed_rec = urlparse(spec)
235
+ target_model = model or (f"{parsed_rec.netloc}{parsed_rec.path}" if parsed_rec.netloc else None) or "nvidia/NVIDIA-Nemotron-Labs-Dory1.0-4B-BF16"
236
+ else:
237
+ target_model = model or "nvidia/NVIDIA-Nemotron-Labs-Dory1.0-4B-BF16"
238
+ return RecurrentPyTorchBackend(model_name=target_model, **kwargs)
239
+
240
+ # Fallback to Ollama or Stub
241
+ if "stub" in spec:
242
+ return StubBackend(**kwargs)
243
+
244
+ from logitgate.backends.ollama import OllamaBackend
245
+ return OllamaBackend(model=model or spec, **kwargs)
246
+
247
+
248
+ def resolve_generative_backend(
249
+ backend_spec: str | GenerativeBackend | None = None,
250
+ model: str | None = None,
251
+ **kwargs: Any,
252
+ ) -> GenerativeBackend:
253
+ if isinstance(backend_spec, GenerativeBackend):
254
+ return backend_spec
255
+
256
+ backend = resolve_logit_backend(backend_spec, model=model, **kwargs)
257
+ if isinstance(backend, GenerativeBackend):
258
+ return backend
259
+ raise TypeError(f"Backend {backend} does not implement GenerativeBackend")