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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from logitgate.backends.base import (
|
|
9
|
+
GenerativeBackend,
|
|
10
|
+
LogitBackend,
|
|
11
|
+
strip_reasoning_blocks,
|
|
12
|
+
)
|
|
13
|
+
from logitgate.config import LogitGateDefaults
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OllamaBackend(LogitBackend, GenerativeBackend):
|
|
17
|
+
"""
|
|
18
|
+
Ollama backend supporting first-token logprob extraction with reasoning suppression
|
|
19
|
+
(think: False), persistent connection pooling, and generative diagnostic critiques.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
base_url: str = "http://localhost:11434",
|
|
25
|
+
model: str = "qwen2.5-coder:14b",
|
|
26
|
+
timeout: float = 30.0,
|
|
27
|
+
num_ctx: int | None = None,
|
|
28
|
+
options: dict[str, Any] | None = None,
|
|
29
|
+
keep_alive: str | int | None = None,
|
|
30
|
+
client: httpx.Client | None = None,
|
|
31
|
+
async_client: httpx.AsyncClient | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.base_url = base_url.rstrip("/")
|
|
34
|
+
self.model = model
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
eff_num_ctx = num_ctx if num_ctx is not None else (LogitGateDefaults.num_ctx or 16384)
|
|
37
|
+
self.num_ctx = eff_num_ctx
|
|
38
|
+
self.options = dict(options) if options else {}
|
|
39
|
+
self.keep_alive = keep_alive
|
|
40
|
+
self._client = client
|
|
41
|
+
self._async_client = async_client
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"OllamaBackend(model='{self.model}', base_url='{self.base_url}')"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def client(self) -> httpx.Client:
|
|
48
|
+
if self._client is None or self._client.is_closed:
|
|
49
|
+
self._client = httpx.Client(timeout=self.timeout)
|
|
50
|
+
return self._client
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def async_client(self) -> httpx.AsyncClient:
|
|
54
|
+
if self._async_client is None or self._async_client.is_closed:
|
|
55
|
+
self._async_client = httpx.AsyncClient(timeout=self.timeout)
|
|
56
|
+
return self._async_client
|
|
57
|
+
|
|
58
|
+
def close(self) -> None:
|
|
59
|
+
"""Closes the synchronous HTTP client session if initialized."""
|
|
60
|
+
if self._client is not None and not self._client.is_closed:
|
|
61
|
+
self._client.close()
|
|
62
|
+
|
|
63
|
+
async def aclose(self) -> None:
|
|
64
|
+
"""Closes the asynchronous HTTP client session if initialized."""
|
|
65
|
+
if self._async_client is not None and not self._async_client.is_closed:
|
|
66
|
+
await self._async_client.aclose()
|
|
67
|
+
|
|
68
|
+
def __enter__(self) -> Self:
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, *args: object) -> None:
|
|
72
|
+
self.close()
|
|
73
|
+
|
|
74
|
+
async def __aenter__(self) -> Self:
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def __aexit__(self, *args: object) -> None:
|
|
78
|
+
await self.aclose()
|
|
79
|
+
|
|
80
|
+
def build_query_payload(
|
|
81
|
+
self,
|
|
82
|
+
prompt: str,
|
|
83
|
+
top_k: int = 10,
|
|
84
|
+
allowed_tokens: list[str] | None = None,
|
|
85
|
+
**kwargs: Any,
|
|
86
|
+
) -> dict[str, Any]:
|
|
87
|
+
"""Constructs the chat completion payload ensuring think: False."""
|
|
88
|
+
messages = [
|
|
89
|
+
{
|
|
90
|
+
"role": "system",
|
|
91
|
+
"content": "Respond with a single character only. Do not think or explain.",
|
|
92
|
+
},
|
|
93
|
+
{"role": "user", "content": prompt},
|
|
94
|
+
]
|
|
95
|
+
opts: dict[str, Any] = {
|
|
96
|
+
"num_predict": 1,
|
|
97
|
+
"temperature": 0.0,
|
|
98
|
+
"num_ctx": self.num_ctx,
|
|
99
|
+
}
|
|
100
|
+
if self.options:
|
|
101
|
+
opts.update(self.options)
|
|
102
|
+
payload: dict[str, Any] = {
|
|
103
|
+
"model": self.model,
|
|
104
|
+
"messages": messages,
|
|
105
|
+
"stream": False,
|
|
106
|
+
"think": False,
|
|
107
|
+
"logprobs": True,
|
|
108
|
+
"top_logprobs": top_k,
|
|
109
|
+
"options": opts,
|
|
110
|
+
}
|
|
111
|
+
if self.keep_alive is not None:
|
|
112
|
+
payload["keep_alive"] = self.keep_alive
|
|
113
|
+
for k, v in kwargs.items():
|
|
114
|
+
if k == "options" and isinstance(v, dict):
|
|
115
|
+
payload["options"].update(v)
|
|
116
|
+
elif k not in payload:
|
|
117
|
+
payload[k] = v
|
|
118
|
+
return payload
|
|
119
|
+
|
|
120
|
+
def _extract_top_logprobs_from_response(self, data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
121
|
+
lp = data.get("logprobs") or data.get("message", {}).get("logprobs")
|
|
122
|
+
if not lp or not isinstance(lp, list):
|
|
123
|
+
return []
|
|
124
|
+
first = lp[0]
|
|
125
|
+
if isinstance(first, dict):
|
|
126
|
+
if "top_logprobs" in first:
|
|
127
|
+
return first["top_logprobs"]
|
|
128
|
+
if "token" in first:
|
|
129
|
+
return lp
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
def query_top_logprobs(
|
|
133
|
+
self,
|
|
134
|
+
prompt: str,
|
|
135
|
+
top_k: int = 10,
|
|
136
|
+
allowed_tokens: list[str] | None = None,
|
|
137
|
+
**kwargs: Any,
|
|
138
|
+
) -> list[dict[str, Any]]:
|
|
139
|
+
payload = self.build_query_payload(
|
|
140
|
+
prompt, top_k=top_k, allowed_tokens=allowed_tokens, **kwargs
|
|
141
|
+
)
|
|
142
|
+
resp = self.client.post(f"{self.base_url}/api/chat", json=payload)
|
|
143
|
+
resp.raise_for_status()
|
|
144
|
+
data = resp.json()
|
|
145
|
+
return self._extract_top_logprobs_from_response(data)
|
|
146
|
+
|
|
147
|
+
async def aquery_top_logprobs(
|
|
148
|
+
self,
|
|
149
|
+
prompt: str,
|
|
150
|
+
top_k: int = 10,
|
|
151
|
+
allowed_tokens: list[str] | None = None,
|
|
152
|
+
**kwargs: Any,
|
|
153
|
+
) -> list[dict[str, Any]]:
|
|
154
|
+
payload = self.build_query_payload(
|
|
155
|
+
prompt, top_k=top_k, allowed_tokens=allowed_tokens, **kwargs
|
|
156
|
+
)
|
|
157
|
+
resp = await self.async_client.post(f"{self.base_url}/api/chat", json=payload)
|
|
158
|
+
resp.raise_for_status()
|
|
159
|
+
data = resp.json()
|
|
160
|
+
return self._extract_top_logprobs_from_response(data)
|
|
161
|
+
|
|
162
|
+
def generate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
|
|
163
|
+
opts: dict[str, Any] = {
|
|
164
|
+
"temperature": 0.0,
|
|
165
|
+
"num_ctx": self.num_ctx,
|
|
166
|
+
}
|
|
167
|
+
if max_tokens is not None:
|
|
168
|
+
opts["num_predict"] = max_tokens
|
|
169
|
+
if self.options:
|
|
170
|
+
opts.update(self.options)
|
|
171
|
+
think_default = max_tokens is None or max_tokens >= 256
|
|
172
|
+
think_val = kwargs.pop("think", think_default)
|
|
173
|
+
if think_val is None:
|
|
174
|
+
think_val = think_default
|
|
175
|
+
payload: dict[str, Any] = {
|
|
176
|
+
"model": self.model,
|
|
177
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
178
|
+
"stream": False,
|
|
179
|
+
"think": think_val,
|
|
180
|
+
"options": opts,
|
|
181
|
+
}
|
|
182
|
+
if self.keep_alive is not None:
|
|
183
|
+
payload["keep_alive"] = self.keep_alive
|
|
184
|
+
for k, v in kwargs.items():
|
|
185
|
+
if k == "options" and isinstance(v, dict):
|
|
186
|
+
payload["options"].update(v)
|
|
187
|
+
else:
|
|
188
|
+
payload[k] = v
|
|
189
|
+
resp = self.client.post(f"{self.base_url}/api/chat", json=payload)
|
|
190
|
+
if resp.status_code == 400 and think_val:
|
|
191
|
+
# Fallback for non-thinking models (e.g. qwen2.5, llama3) that reject think=True
|
|
192
|
+
payload["think"] = False
|
|
193
|
+
think_val = False
|
|
194
|
+
resp = self.client.post(f"{self.base_url}/api/chat", json=payload)
|
|
195
|
+
resp.raise_for_status()
|
|
196
|
+
data = resp.json()
|
|
197
|
+
content = data.get("message", {}).get("content", "")
|
|
198
|
+
tokens = data.get("eval_count", 0)
|
|
199
|
+
visible_text = strip_reasoning_blocks(content)
|
|
200
|
+
|
|
201
|
+
# Mid-Thought Truncation Safety Net: True Continuation
|
|
202
|
+
# If thinking was enabled and 100% of generated tokens were consumed inside <think>,
|
|
203
|
+
# visible_text is empty while raw content was non-empty. Close </think> and continue
|
|
204
|
+
# with think=False so all max_tokens go to visible diagnostic output while preserving CoT.
|
|
205
|
+
if not visible_text and content and think_val:
|
|
206
|
+
raw_thinking = content.rstrip()
|
|
207
|
+
assistant_prefill = raw_thinking + "\n</think>\n"
|
|
208
|
+
continuation_opts = dict(opts)
|
|
209
|
+
if max_tokens is not None:
|
|
210
|
+
continuation_opts["num_predict"] = max_tokens
|
|
211
|
+
continuation_payload = dict(payload)
|
|
212
|
+
continuation_payload["messages"] = [
|
|
213
|
+
{"role": "user", "content": prompt},
|
|
214
|
+
{"role": "assistant", "content": assistant_prefill},
|
|
215
|
+
]
|
|
216
|
+
continuation_payload["think"] = False
|
|
217
|
+
continuation_payload["options"] = continuation_opts
|
|
218
|
+
resp_cont = self.client.post(f"{self.base_url}/api/chat", json=continuation_payload)
|
|
219
|
+
resp_cont.raise_for_status()
|
|
220
|
+
data_cont = resp_cont.json()
|
|
221
|
+
content_cont = data_cont.get("message", {}).get("content", "")
|
|
222
|
+
tokens_cont = data_cont.get("eval_count", 0)
|
|
223
|
+
return strip_reasoning_blocks(content_cont), tokens + tokens_cont
|
|
224
|
+
|
|
225
|
+
return visible_text, tokens
|
|
226
|
+
|
|
227
|
+
async def agenerate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
|
|
228
|
+
opts: dict[str, Any] = {
|
|
229
|
+
"temperature": 0.0,
|
|
230
|
+
"num_ctx": self.num_ctx,
|
|
231
|
+
}
|
|
232
|
+
if max_tokens is not None:
|
|
233
|
+
opts["num_predict"] = max_tokens
|
|
234
|
+
if self.options:
|
|
235
|
+
opts.update(self.options)
|
|
236
|
+
think_default = max_tokens is None or max_tokens >= 256
|
|
237
|
+
think_val = kwargs.pop("think", think_default)
|
|
238
|
+
if think_val is None:
|
|
239
|
+
think_val = think_default
|
|
240
|
+
payload: dict[str, Any] = {
|
|
241
|
+
"model": self.model,
|
|
242
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
243
|
+
"stream": False,
|
|
244
|
+
"think": think_val,
|
|
245
|
+
"options": opts,
|
|
246
|
+
}
|
|
247
|
+
if self.keep_alive is not None:
|
|
248
|
+
payload["keep_alive"] = self.keep_alive
|
|
249
|
+
for k, v in kwargs.items():
|
|
250
|
+
if k == "options" and isinstance(v, dict):
|
|
251
|
+
payload["options"].update(v)
|
|
252
|
+
else:
|
|
253
|
+
payload[k] = v
|
|
254
|
+
resp = await self.async_client.post(f"{self.base_url}/api/chat", json=payload)
|
|
255
|
+
if resp.status_code == 400 and think_val:
|
|
256
|
+
payload["think"] = False
|
|
257
|
+
think_val = False
|
|
258
|
+
resp = await self.async_client.post(f"{self.base_url}/api/chat", json=payload)
|
|
259
|
+
resp.raise_for_status()
|
|
260
|
+
data = resp.json()
|
|
261
|
+
content = data.get("message", {}).get("content", "")
|
|
262
|
+
tokens = data.get("eval_count", 0)
|
|
263
|
+
visible_text = strip_reasoning_blocks(content)
|
|
264
|
+
|
|
265
|
+
# Mid-Thought Truncation Safety Net: True Continuation
|
|
266
|
+
if not visible_text and content and think_val:
|
|
267
|
+
raw_thinking = content.rstrip()
|
|
268
|
+
assistant_prefill = raw_thinking + "\n</think>\n"
|
|
269
|
+
continuation_opts = dict(opts)
|
|
270
|
+
if max_tokens is not None:
|
|
271
|
+
continuation_opts["num_predict"] = max_tokens
|
|
272
|
+
continuation_payload = dict(payload)
|
|
273
|
+
continuation_payload["messages"] = [
|
|
274
|
+
{"role": "user", "content": prompt},
|
|
275
|
+
{"role": "assistant", "content": assistant_prefill},
|
|
276
|
+
]
|
|
277
|
+
continuation_payload["think"] = False
|
|
278
|
+
continuation_payload["options"] = continuation_opts
|
|
279
|
+
resp_cont = await self.async_client.post(f"{self.base_url}/api/chat", json=continuation_payload)
|
|
280
|
+
resp_cont.raise_for_status()
|
|
281
|
+
data_cont = resp_cont.json()
|
|
282
|
+
content_cont = data_cont.get("message", {}).get("content", "")
|
|
283
|
+
tokens_cont = data_cont.get("eval_count", 0)
|
|
284
|
+
return strip_reasoning_blocks(content_cont), tokens + tokens_cont
|
|
285
|
+
|
|
286
|
+
return visible_text, tokens
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from logitgate.backends.base import (
|
|
10
|
+
GenerativeBackend,
|
|
11
|
+
LogitBackend,
|
|
12
|
+
strip_reasoning_blocks,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OpenAICompatBackend(LogitBackend, GenerativeBackend):
|
|
17
|
+
"""
|
|
18
|
+
OpenAI-compatible backend (vLLM, Groq, OpenRouter, Together, OpenAI)
|
|
19
|
+
supporting first-token logprob extraction, persistent connection pooling,
|
|
20
|
+
and generative reviews with reasoning trace suppression.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
base_url: str = "https://api.openai.com/v1",
|
|
26
|
+
model: str = "gpt-4o-mini",
|
|
27
|
+
api_key: str | None = None,
|
|
28
|
+
timeout: float = 30.0,
|
|
29
|
+
logit_bias: dict[str, int | float] | None = None,
|
|
30
|
+
client: httpx.Client | None = None,
|
|
31
|
+
async_client: httpx.AsyncClient | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.base_url = base_url.rstrip("/")
|
|
34
|
+
self.model = model
|
|
35
|
+
self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "sk-dummy")
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
self.logit_bias = logit_bias
|
|
38
|
+
self._client = client
|
|
39
|
+
self._async_client = async_client
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return f"OpenAICompatBackend(model='{self.model}', base_url='{self.base_url}')"
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def client(self) -> httpx.Client:
|
|
46
|
+
if self._client is None or self._client.is_closed:
|
|
47
|
+
self._client = httpx.Client(timeout=self.timeout)
|
|
48
|
+
return self._client
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def async_client(self) -> httpx.AsyncClient:
|
|
52
|
+
if self._async_client is None or self._async_client.is_closed:
|
|
53
|
+
self._async_client = httpx.AsyncClient(timeout=self.timeout)
|
|
54
|
+
return self._async_client
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
"""Closes the synchronous HTTP client session if initialized."""
|
|
58
|
+
if self._client is not None and not self._client.is_closed:
|
|
59
|
+
self._client.close()
|
|
60
|
+
|
|
61
|
+
async def aclose(self) -> None:
|
|
62
|
+
"""Closes the asynchronous HTTP client session if initialized."""
|
|
63
|
+
if self._async_client is not None and not self._async_client.is_closed:
|
|
64
|
+
await self._async_client.aclose()
|
|
65
|
+
|
|
66
|
+
def __enter__(self) -> Self:
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def __exit__(self, *args: object) -> None:
|
|
70
|
+
self.close()
|
|
71
|
+
|
|
72
|
+
async def __aenter__(self) -> Self:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
async def __aexit__(self, *args: object) -> None:
|
|
76
|
+
await self.aclose()
|
|
77
|
+
|
|
78
|
+
def _headers(self) -> dict[str, str]:
|
|
79
|
+
headers = {"Content-Type": "application/json"}
|
|
80
|
+
if self.api_key:
|
|
81
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
82
|
+
return headers
|
|
83
|
+
|
|
84
|
+
def build_query_payload(
|
|
85
|
+
self,
|
|
86
|
+
prompt: str,
|
|
87
|
+
top_k: int = 10,
|
|
88
|
+
allowed_tokens: list[str] | None = None,
|
|
89
|
+
**kwargs: Any,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
payload: dict[str, Any] = {
|
|
92
|
+
"model": self.model,
|
|
93
|
+
"messages": [
|
|
94
|
+
{
|
|
95
|
+
"role": "system",
|
|
96
|
+
"content": "Respond with a single character only.",
|
|
97
|
+
},
|
|
98
|
+
{"role": "user", "content": prompt},
|
|
99
|
+
],
|
|
100
|
+
"max_tokens": 1,
|
|
101
|
+
"temperature": 0.0,
|
|
102
|
+
"logprobs": True,
|
|
103
|
+
"top_logprobs": min(top_k, 20),
|
|
104
|
+
}
|
|
105
|
+
logit_bias = kwargs.get("logit_bias", self.logit_bias)
|
|
106
|
+
if logit_bias is not None:
|
|
107
|
+
payload["logit_bias"] = logit_bias
|
|
108
|
+
for k, v in kwargs.items():
|
|
109
|
+
if k not in payload and k != "logit_bias":
|
|
110
|
+
payload[k] = v
|
|
111
|
+
return payload
|
|
112
|
+
|
|
113
|
+
def _extract_top_logprobs_from_response(self, data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
114
|
+
choices = data.get("choices", [])
|
|
115
|
+
if not choices:
|
|
116
|
+
return []
|
|
117
|
+
logprobs = choices[0].get("logprobs")
|
|
118
|
+
if not logprobs:
|
|
119
|
+
return []
|
|
120
|
+
content_logprobs = logprobs.get("content", [])
|
|
121
|
+
if not content_logprobs:
|
|
122
|
+
return []
|
|
123
|
+
top_logprobs = content_logprobs[0].get("top_logprobs", [])
|
|
124
|
+
return [
|
|
125
|
+
{"token": item.get("token", ""), "logprob": item.get("logprob", -10.0)}
|
|
126
|
+
for item in top_logprobs
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
def query_top_logprobs(
|
|
130
|
+
self,
|
|
131
|
+
prompt: str,
|
|
132
|
+
top_k: int = 10,
|
|
133
|
+
allowed_tokens: list[str] | None = None,
|
|
134
|
+
**kwargs: Any,
|
|
135
|
+
) -> list[dict[str, Any]]:
|
|
136
|
+
url = f"{self.base_url}/chat/completions"
|
|
137
|
+
payload = self.build_query_payload(
|
|
138
|
+
prompt, top_k=top_k, allowed_tokens=allowed_tokens, **kwargs
|
|
139
|
+
)
|
|
140
|
+
resp = self.client.post(url, json=payload, headers=self._headers())
|
|
141
|
+
resp.raise_for_status()
|
|
142
|
+
data = resp.json()
|
|
143
|
+
return self._extract_top_logprobs_from_response(data)
|
|
144
|
+
|
|
145
|
+
async def aquery_top_logprobs(
|
|
146
|
+
self,
|
|
147
|
+
prompt: str,
|
|
148
|
+
top_k: int = 10,
|
|
149
|
+
allowed_tokens: list[str] | None = None,
|
|
150
|
+
**kwargs: Any,
|
|
151
|
+
) -> list[dict[str, Any]]:
|
|
152
|
+
url = f"{self.base_url}/chat/completions"
|
|
153
|
+
payload = self.build_query_payload(
|
|
154
|
+
prompt, top_k=top_k, allowed_tokens=allowed_tokens, **kwargs
|
|
155
|
+
)
|
|
156
|
+
resp = await self.async_client.post(url, json=payload, headers=self._headers())
|
|
157
|
+
resp.raise_for_status()
|
|
158
|
+
data = resp.json()
|
|
159
|
+
return self._extract_top_logprobs_from_response(data)
|
|
160
|
+
|
|
161
|
+
def generate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
|
|
162
|
+
url = f"{self.base_url}/chat/completions"
|
|
163
|
+
payload: dict[str, Any] = {
|
|
164
|
+
"model": self.model,
|
|
165
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
166
|
+
"temperature": 0.0,
|
|
167
|
+
}
|
|
168
|
+
if max_tokens is not None:
|
|
169
|
+
payload["max_tokens"] = max_tokens
|
|
170
|
+
payload.update(kwargs)
|
|
171
|
+
resp = self.client.post(url, json=payload, headers=self._headers())
|
|
172
|
+
resp.raise_for_status()
|
|
173
|
+
data = resp.json()
|
|
174
|
+
choices = data.get("choices", [])
|
|
175
|
+
content = choices[0].get("message", {}).get("content", "") if choices else ""
|
|
176
|
+
tokens = data.get("usage", {}).get("completion_tokens", 0)
|
|
177
|
+
return strip_reasoning_blocks(content), tokens
|
|
178
|
+
|
|
179
|
+
async def agenerate(self, prompt: str, max_tokens: int | None = None, **kwargs: Any) -> tuple[str, int]:
|
|
180
|
+
url = f"{self.base_url}/chat/completions"
|
|
181
|
+
payload: dict[str, Any] = {
|
|
182
|
+
"model": self.model,
|
|
183
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
184
|
+
"temperature": 0.0,
|
|
185
|
+
}
|
|
186
|
+
if max_tokens is not None:
|
|
187
|
+
payload["max_tokens"] = max_tokens
|
|
188
|
+
payload.update(kwargs)
|
|
189
|
+
resp = await self.async_client.post(url, json=payload, headers=self._headers())
|
|
190
|
+
resp.raise_for_status()
|
|
191
|
+
data = resp.json()
|
|
192
|
+
choices = data.get("choices", [])
|
|
193
|
+
content = choices[0].get("message", {}).get("content", "") if choices else ""
|
|
194
|
+
tokens = data.get("usage", {}).get("completion_tokens", 0)
|
|
195
|
+
return strip_reasoning_blocks(content), tokens
|