jev-compatible-server 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.
- jev_compatible_server/__init__.py +5 -0
- jev_compatible_server/app.py +144 -0
- jev_compatible_server/backends.py +320 -0
- jev_compatible_server/batching.py +68 -0
- jev_compatible_server/bosun.py +229 -0
- jev_compatible_server/causal_options.py +228 -0
- jev_compatible_server/classifier_adapters.py +472 -0
- jev_compatible_server/cross_encoder.py +71 -0
- jev_compatible_server/custom_heads.py +704 -0
- jev_compatible_server/encoder_decoder.py +630 -0
- jev_compatible_server/gliner2.py +40 -0
- jev_compatible_server/hidden_state_probe.py +384 -0
- jev_compatible_server/laya.py +135 -0
- jev_compatible_server/native_systemone.py +248 -0
- jev_compatible_server/protocol.py +104 -0
- jev_compatible_server/public-models.json +981 -0
- jev_compatible_server/registry.py +283 -0
- jev_compatible_server/runtime.py +237 -0
- jev_compatible_server/sequence_classifier.py +219 -0
- jev_compatible_server-0.1.0.dist-info/METADATA +157 -0
- jev_compatible_server-0.1.0.dist-info/RECORD +23 -0
- jev_compatible_server-0.1.0.dist-info/WHEEL +4 -0
- jev_compatible_server-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Adapters for public servers that own their typed-decision readout.
|
|
2
|
+
|
|
3
|
+
These models publish more than a checkpoint: their probability readout is part
|
|
4
|
+
of a separately versioned native server. Forwarding the typed request to that
|
|
5
|
+
server preserves its fixed canvas/branching, calibration, and thinking policy
|
|
6
|
+
instead of substituting a generic next-token approximation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.error import HTTPError, URLError
|
|
16
|
+
from urllib.request import Request, urlopen
|
|
17
|
+
|
|
18
|
+
from .protocol import (
|
|
19
|
+
ChoiceQuestion,
|
|
20
|
+
DecisionRequest,
|
|
21
|
+
DecisionResponse,
|
|
22
|
+
NoulQuestion,
|
|
23
|
+
ScoreQuestion,
|
|
24
|
+
)
|
|
25
|
+
from .runtime import DecisionRuntime, RuntimeErrorBase
|
|
26
|
+
|
|
27
|
+
JsonObject = dict[str, Any]
|
|
28
|
+
PostJSON = Callable[[str, JsonObject, float], JsonObject]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _decision_value(config: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
|
32
|
+
nested = config.get("decision")
|
|
33
|
+
if isinstance(nested, Mapping) and key in nested:
|
|
34
|
+
default = nested[key]
|
|
35
|
+
return config.get(f"decision.{key}", default)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _post_json(url: str, body: JsonObject, timeout_seconds: float) -> JsonObject:
|
|
39
|
+
data = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
40
|
+
request = Request(url, data=data, headers={"Content-Type": "application/json"})
|
|
41
|
+
try:
|
|
42
|
+
with urlopen(request, timeout=timeout_seconds) as response:
|
|
43
|
+
payload = response.read()
|
|
44
|
+
except HTTPError as exc:
|
|
45
|
+
detail = exc.read(1_000).decode("utf-8", errors="replace")
|
|
46
|
+
raise RuntimeErrorBase(
|
|
47
|
+
f"native decision server returned HTTP {exc.code}: {detail}"
|
|
48
|
+
) from exc
|
|
49
|
+
except URLError as exc:
|
|
50
|
+
raise RuntimeErrorBase("native decision server is unavailable") from exc
|
|
51
|
+
try:
|
|
52
|
+
decoded = json.loads(payload)
|
|
53
|
+
except json.JSONDecodeError as exc:
|
|
54
|
+
raise RuntimeErrorBase("native decision server returned invalid JSON") from exc
|
|
55
|
+
if not isinstance(decoded, dict):
|
|
56
|
+
raise RuntimeErrorBase("native decision server returned a non-object response")
|
|
57
|
+
return decoded
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NativeSystemOneHTTPRuntime(DecisionRuntime):
|
|
61
|
+
"""Forward the exact typed request to a model's published server.
|
|
62
|
+
|
|
63
|
+
Required config: ``decision.endpoint``. ``decision.native_model`` selects
|
|
64
|
+
the server-side alias, ``decision.request_fields`` supplies documented
|
|
65
|
+
request extensions (for example OpenJev's ``think``), and
|
|
66
|
+
``decision.timeout_seconds`` bounds each native read.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
model_id: str,
|
|
72
|
+
*,
|
|
73
|
+
config: Mapping[str, Any] | None = None,
|
|
74
|
+
post_json: PostJSON | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
self.config = dict(config or {})
|
|
77
|
+
endpoint = _decision_value(self.config, "endpoint")
|
|
78
|
+
if not isinstance(endpoint, str) or not endpoint.startswith(("http://", "https://")):
|
|
79
|
+
raise RuntimeErrorBase("native_systemone requires decision.endpoint as an HTTP URL")
|
|
80
|
+
native_model = _decision_value(self.config, "native_model", model_id)
|
|
81
|
+
if not isinstance(native_model, str) or not native_model:
|
|
82
|
+
raise RuntimeErrorBase("decision.native_model must be a non-empty string")
|
|
83
|
+
fields = _decision_value(self.config, "request_fields", {})
|
|
84
|
+
if not isinstance(fields, Mapping):
|
|
85
|
+
raise RuntimeErrorBase("decision.request_fields must be an object")
|
|
86
|
+
timeout = _decision_value(self.config, "timeout_seconds", 120.0)
|
|
87
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
|
|
88
|
+
raise RuntimeErrorBase("decision.timeout_seconds must be a positive number")
|
|
89
|
+
self.model_name = str(_decision_value(self.config, "public_model_name", model_id))
|
|
90
|
+
self.endpoint = endpoint
|
|
91
|
+
self.native_model = native_model
|
|
92
|
+
self.request_fields = dict(fields)
|
|
93
|
+
self.timeout_seconds = float(timeout)
|
|
94
|
+
self._post_json = post_json or _post_json
|
|
95
|
+
|
|
96
|
+
def _body(self, request: DecisionRequest) -> JsonObject:
|
|
97
|
+
body = request.model_dump(mode="json")
|
|
98
|
+
body["model"] = self.native_model
|
|
99
|
+
for key, value in self.request_fields.items():
|
|
100
|
+
if key in {"state", "questions", "model"}:
|
|
101
|
+
raise RuntimeErrorBase(
|
|
102
|
+
"decision.request_fields may not override state, questions, or model"
|
|
103
|
+
)
|
|
104
|
+
body[str(key)] = value
|
|
105
|
+
return body
|
|
106
|
+
|
|
107
|
+
def decide_batch(self, requests: Sequence[DecisionRequest]) -> list[DecisionResponse]:
|
|
108
|
+
return [
|
|
109
|
+
self._decode(
|
|
110
|
+
request,
|
|
111
|
+
self._post_json(self.endpoint, self._body(request), self.timeout_seconds),
|
|
112
|
+
)
|
|
113
|
+
for request in requests
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def _decode(self, request: DecisionRequest, raw: JsonObject) -> DecisionResponse:
|
|
117
|
+
raw_answers = raw.get("answers")
|
|
118
|
+
if not isinstance(raw_answers, Mapping) or set(raw_answers) != set(request.questions):
|
|
119
|
+
raise RuntimeErrorBase(
|
|
120
|
+
"native decision server did not return exactly one answer per question"
|
|
121
|
+
)
|
|
122
|
+
normalized: JsonObject = {
|
|
123
|
+
"model": self.model_name,
|
|
124
|
+
"answers": {},
|
|
125
|
+
"usage": raw.get("usage", {}),
|
|
126
|
+
}
|
|
127
|
+
for name, question in request.questions.items():
|
|
128
|
+
answer = raw_answers[name]
|
|
129
|
+
if not isinstance(answer, Mapping) or answer.get("type") != question.type:
|
|
130
|
+
raise RuntimeErrorBase(
|
|
131
|
+
f"native server returned an invalid {question.type} answer for {name!r}"
|
|
132
|
+
)
|
|
133
|
+
value = dict(answer)
|
|
134
|
+
if isinstance(question, ScoreQuestion) and isinstance(value.get("legend"), Mapping):
|
|
135
|
+
legend = value["legend"]
|
|
136
|
+
if any(str(index) not in legend for index in range(len(question.criteria))):
|
|
137
|
+
raise RuntimeErrorBase(
|
|
138
|
+
f"native server returned an incomplete score legend for {name!r}"
|
|
139
|
+
)
|
|
140
|
+
value["legend"] = [legend[str(index)] for index in range(len(question.criteria))]
|
|
141
|
+
self._validate_answer(question, value, name)
|
|
142
|
+
normalized["answers"][name] = value
|
|
143
|
+
try:
|
|
144
|
+
response = DecisionResponse.model_validate(normalized)
|
|
145
|
+
except ValueError as exc:
|
|
146
|
+
raise RuntimeErrorBase("native decision server returned an invalid typed response") from exc
|
|
147
|
+
usage = response.usage
|
|
148
|
+
if usage.input_tokens < 0 or usage.output_tokens < 0:
|
|
149
|
+
raise RuntimeErrorBase("native decision server returned negative token usage")
|
|
150
|
+
return response
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _validate_answer(question: Any, answer: Mapping[str, Any], name: str) -> None:
|
|
154
|
+
if isinstance(question, NoulQuestion):
|
|
155
|
+
value = answer.get("noul")
|
|
156
|
+
if (
|
|
157
|
+
not isinstance(value, (int, float))
|
|
158
|
+
or isinstance(value, bool)
|
|
159
|
+
or not 0.0 <= float(value) <= 1.0
|
|
160
|
+
):
|
|
161
|
+
raise RuntimeErrorBase(f"native server returned invalid noul probability for {name!r}")
|
|
162
|
+
return
|
|
163
|
+
probabilities = answer.get("probabilities")
|
|
164
|
+
expected = (
|
|
165
|
+
list(question.criteria)
|
|
166
|
+
if isinstance(question, ChoiceQuestion)
|
|
167
|
+
else [str(index) for index in range(len(question.criteria))]
|
|
168
|
+
)
|
|
169
|
+
if not isinstance(probabilities, Mapping) or set(probabilities) != set(expected):
|
|
170
|
+
raise RuntimeErrorBase(f"native server returned a misaligned probability distribution for {name!r}")
|
|
171
|
+
values = [probabilities[key] for key in expected]
|
|
172
|
+
if any(
|
|
173
|
+
not isinstance(value, (int, float))
|
|
174
|
+
or isinstance(value, bool)
|
|
175
|
+
or not math.isfinite(float(value))
|
|
176
|
+
or float(value) < 0.0
|
|
177
|
+
for value in values
|
|
178
|
+
):
|
|
179
|
+
raise RuntimeErrorBase(f"native server returned invalid probabilities for {name!r}")
|
|
180
|
+
if not math.isclose(sum(float(value) for value in values), 1.0, rel_tol=1e-5, abs_tol=1e-5):
|
|
181
|
+
raise RuntimeErrorBase(f"native server probabilities do not sum to one for {name!r}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class DjevHTTPRuntime(NativeSystemOneHTTPRuntime):
|
|
185
|
+
"""DJeV's native ``/v1/request`` contract and structured distributions."""
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
model_id: str,
|
|
190
|
+
*,
|
|
191
|
+
config: Mapping[str, Any] | None = None,
|
|
192
|
+
post_json: PostJSON | None = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
merged = dict(config or {})
|
|
195
|
+
decision = dict(merged.get("decision", {})) if isinstance(merged.get("decision"), Mapping) else {}
|
|
196
|
+
decision.setdefault("native_model", "djev")
|
|
197
|
+
merged["decision"] = decision
|
|
198
|
+
super().__init__(model_id, config=merged, post_json=post_json)
|
|
199
|
+
|
|
200
|
+
def _body(self, request: DecisionRequest) -> JsonObject:
|
|
201
|
+
body = super()._body(request)
|
|
202
|
+
options = _decision_value(self.config, "options", {})
|
|
203
|
+
if not isinstance(options, Mapping):
|
|
204
|
+
raise RuntimeErrorBase("decision.options must be an object")
|
|
205
|
+
body["options"] = dict(options)
|
|
206
|
+
return body
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class DjevThinkingRuntime(DecisionRuntime):
|
|
210
|
+
"""Reject the unshipped DJeV thinking variant without fabricating a readout."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, model_id: str, *, config: Mapping[str, Any] | None = None) -> None:
|
|
213
|
+
del config
|
|
214
|
+
self.model_name = model_id
|
|
215
|
+
|
|
216
|
+
def decide_batch(self, requests: Sequence[DecisionRequest]) -> list[DecisionResponse]:
|
|
217
|
+
del requests
|
|
218
|
+
raise RuntimeErrorBase(
|
|
219
|
+
"DJeV publishes no thinking inference contract; the public runtime exposes only structured reads"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class OpenJevThinkingHTTPRuntime(NativeSystemOneHTTPRuntime):
|
|
224
|
+
"""OpenJev's documented ``think`` extension over native typed reads."""
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
model_id: str,
|
|
229
|
+
*,
|
|
230
|
+
config: Mapping[str, Any] | None = None,
|
|
231
|
+
post_json: PostJSON | None = None,
|
|
232
|
+
) -> None:
|
|
233
|
+
super().__init__(model_id, config=config, post_json=post_json)
|
|
234
|
+
if "think" not in self.request_fields:
|
|
235
|
+
raise RuntimeErrorBase(
|
|
236
|
+
"openjev_thinking requires decision.request_fields.think as a token budget"
|
|
237
|
+
)
|
|
238
|
+
think = self.request_fields["think"]
|
|
239
|
+
if isinstance(think, bool) or not isinstance(think, int) or not 0 <= think <= 4096:
|
|
240
|
+
raise RuntimeErrorBase("OpenJev think must be an integer from 0 through 4096")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class JeffHTTPRuntime(NativeSystemOneHTTPRuntime):
|
|
244
|
+
"""Jeff's GLiFormer server, including its native grouping and temperature."""
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class WinnowHTTPRuntime(NativeSystemOneHTTPRuntime):
|
|
248
|
+
"""Winnow's llama.cpp server, retaining its shared-prefix branch planner."""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""The wire contract shared with Jev clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
8
|
+
|
|
9
|
+
Content = str | dict[str, Any] | list[Any]
|
|
10
|
+
QuestionType = Literal["choice", "score", "noul"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChoiceQuestion(BaseModel):
|
|
14
|
+
model_config = ConfigDict(extra="forbid")
|
|
15
|
+
|
|
16
|
+
type: Literal["choice"]
|
|
17
|
+
instructions: Content
|
|
18
|
+
criteria: dict[str, Content | None] = Field(min_length=2, max_length=255)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ScoreQuestion(BaseModel):
|
|
22
|
+
model_config = ConfigDict(extra="forbid")
|
|
23
|
+
|
|
24
|
+
type: Literal["score"]
|
|
25
|
+
instructions: Content
|
|
26
|
+
criteria: list[Content] = Field(min_length=2, max_length=255)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoulCriteria(BaseModel):
|
|
30
|
+
model_config = ConfigDict(extra="forbid")
|
|
31
|
+
|
|
32
|
+
true: Content
|
|
33
|
+
false: Content
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NoulQuestion(BaseModel):
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
38
|
+
|
|
39
|
+
type: Literal["noul"]
|
|
40
|
+
instructions: Content
|
|
41
|
+
criteria: NoulCriteria | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
Question = ChoiceQuestion | ScoreQuestion | NoulQuestion
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DecisionRequest(BaseModel):
|
|
48
|
+
"""A Jev-compatible request: one state and any number of named questions."""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(extra="forbid")
|
|
51
|
+
|
|
52
|
+
model: str | None = None
|
|
53
|
+
state: Any
|
|
54
|
+
questions: dict[str, Question] = Field(min_length=1)
|
|
55
|
+
|
|
56
|
+
@model_validator(mode="after")
|
|
57
|
+
def validate_question_names(self) -> DecisionRequest:
|
|
58
|
+
for name in self.questions:
|
|
59
|
+
if not name or len(name) > 128:
|
|
60
|
+
raise ValueError("question names must be 1-128 characters")
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ChoiceAnswer(BaseModel):
|
|
65
|
+
type: Literal["choice"]
|
|
66
|
+
choice: str
|
|
67
|
+
probabilities: dict[str, float]
|
|
68
|
+
confidence: float
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ScoreAnswer(BaseModel):
|
|
72
|
+
type: Literal["score"]
|
|
73
|
+
score: float
|
|
74
|
+
probabilities: dict[str, float]
|
|
75
|
+
confidence: float
|
|
76
|
+
legend: list[Content] | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class NoulAnswer(BaseModel):
|
|
80
|
+
type: Literal["noul"]
|
|
81
|
+
noul: float
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class UnsupportedAnswer(BaseModel):
|
|
85
|
+
"""Per-question capability result for partially compatible models."""
|
|
86
|
+
|
|
87
|
+
type: Literal["unsupported"]
|
|
88
|
+
question_type: QuestionType
|
|
89
|
+
reason: Literal["question_type_not_supported"] = "question_type_not_supported"
|
|
90
|
+
supported_types: list[QuestionType]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
Answer = ChoiceAnswer | ScoreAnswer | NoulAnswer | UnsupportedAnswer
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Usage(BaseModel):
|
|
97
|
+
input_tokens: int = 0
|
|
98
|
+
output_tokens: int = 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DecisionResponse(BaseModel):
|
|
102
|
+
model: str
|
|
103
|
+
answers: dict[str, Answer]
|
|
104
|
+
usage: Usage = Field(default_factory=Usage)
|