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 +103 -0
- logitgate/backends/__init__.py +25 -0
- logitgate/backends/base.py +259 -0
- logitgate/backends/dory_pytorch.py +330 -0
- logitgate/backends/llamacpp.py +352 -0
- logitgate/backends/ollama.py +286 -0
- logitgate/backends/openai_compat.py +195 -0
- logitgate/backends/recurrent.py +245 -0
- logitgate/cascade.py +394 -0
- logitgate/cli.py +264 -0
- logitgate/config.py +172 -0
- logitgate/debiasing/__init__.py +35 -0
- logitgate/debiasing/inversion.py +180 -0
- logitgate/debiasing/permutation.py +287 -0
- logitgate/debiasing/softmax.py +98 -0
- logitgate/gates/__init__.py +14 -0
- logitgate/gates/ast_gate.py +544 -0
- logitgate/gates/base.py +68 -0
- logitgate/gates/judge_gate.py +318 -0
- logitgate/gates/logit_gate.py +951 -0
- logitgate/integrations/__init__.py +10 -0
- logitgate/integrations/deepeval.py +212 -0
- logitgate/integrations/langgraph.py +30 -0
- logitgate/integrations/pytest_plugin.py +68 -0
- logitgate/models.py +109 -0
- logitgate/py.typed +0 -0
- logitgate/snapshot.py +224 -0
- logitgate-0.2.0.dist-info/METADATA +1236 -0
- logitgate-0.2.0.dist-info/RECORD +33 -0
- logitgate-0.2.0.dist-info/WHEEL +5 -0
- logitgate-0.2.0.dist-info/entry_points.txt +5 -0
- logitgate-0.2.0.dist-info/licenses/LICENSE +201 -0
- logitgate-0.2.0.dist-info/top_level.txt +1 -0
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")
|