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.
@@ -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)