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,283 @@
|
|
|
1
|
+
"""Configurable model registry for models with stale or incomplete metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from importlib import resources
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from .backends import (
|
|
15
|
+
LlamaBackend,
|
|
16
|
+
PointerTransformersBackend,
|
|
17
|
+
TransformersBackend,
|
|
18
|
+
load_decision_config,
|
|
19
|
+
)
|
|
20
|
+
from .bosun import BosunDecisionBackend
|
|
21
|
+
from .causal_options import CausalOptionsBackend
|
|
22
|
+
from .classifier_adapters import GLiClassCalibratedBackend, NLIEntailmentBackend
|
|
23
|
+
from .cross_encoder import CrossEncoderBackend
|
|
24
|
+
from .custom_heads import OpenJevScalarHeadBackend, SmallJevSemanticBackend
|
|
25
|
+
from .encoder_decoder import EncoderDecoderMarginBackend, decision_metadata
|
|
26
|
+
from .gliner2 import GLiNER2Runtime
|
|
27
|
+
from .hidden_state_probe import HiddenStateProbeBackend
|
|
28
|
+
from .laya import LayaBackend
|
|
29
|
+
from .native_systemone import (
|
|
30
|
+
DjevHTTPRuntime,
|
|
31
|
+
DjevThinkingRuntime,
|
|
32
|
+
JeffHTTPRuntime,
|
|
33
|
+
OpenJevThinkingHTTPRuntime,
|
|
34
|
+
WinnowHTTPRuntime,
|
|
35
|
+
)
|
|
36
|
+
from .protocol import DecisionRequest, DecisionResponse, UnsupportedAnswer, Usage
|
|
37
|
+
from .runtime import (
|
|
38
|
+
DecisionRuntime,
|
|
39
|
+
RuntimeErrorBase,
|
|
40
|
+
apply_question_type_support,
|
|
41
|
+
configured_question_types,
|
|
42
|
+
)
|
|
43
|
+
from .sequence_classifier import SequenceClassifierMarginBackend
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RegistryModel(BaseModel):
|
|
47
|
+
"""One deployable model and its service-owned decision metadata."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(extra="forbid")
|
|
50
|
+
|
|
51
|
+
backend: Literal["llama", "transformers", "mlx"]
|
|
52
|
+
model: str
|
|
53
|
+
recipe: str | None = None
|
|
54
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
55
|
+
config_path: str | None = None
|
|
56
|
+
enabled: bool = True
|
|
57
|
+
description: str | None = None
|
|
58
|
+
support_status: Literal["supported", "pending"] = "supported"
|
|
59
|
+
|
|
60
|
+
def resolved_config(
|
|
61
|
+
self, recipe_config: dict[str, Any] | None = None
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
resolved = _merge_config({}, recipe_config or {})
|
|
64
|
+
file_config = load_decision_config(self.config_path) if self.config_path else {}
|
|
65
|
+
# Registry values win over model-published metadata and file defaults.
|
|
66
|
+
resolved = _merge_config(resolved, file_config)
|
|
67
|
+
return _merge_config(resolved, self.config)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RegistryFile(BaseModel):
|
|
71
|
+
model_config = ConfigDict(extra="forbid")
|
|
72
|
+
|
|
73
|
+
default: str | None = None
|
|
74
|
+
recipes: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
75
|
+
models: dict[str, RegistryModel] = Field(min_length=1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _merge_config(base: dict[str, Any], override: Mapping[str, Any]) -> dict[str, Any]:
|
|
79
|
+
result = dict(base)
|
|
80
|
+
for key, value in override.items():
|
|
81
|
+
existing = result.get(key)
|
|
82
|
+
if isinstance(existing, dict) and isinstance(value, dict):
|
|
83
|
+
result[key] = _merge_config(existing, value)
|
|
84
|
+
else:
|
|
85
|
+
result[key] = value
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ModelRegistry:
|
|
90
|
+
def __init__(self, definition: RegistryFile):
|
|
91
|
+
self.definition = definition
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_file(cls, path: str | Path) -> ModelRegistry:
|
|
95
|
+
return cls.from_json(Path(path).read_text())
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_builtin(cls) -> ModelRegistry:
|
|
99
|
+
resource = resources.files("jev_compatible_server").joinpath("public-models.json")
|
|
100
|
+
try:
|
|
101
|
+
value = resource.read_text()
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
source_path = Path(__file__).parents[2] / "configs" / "public-models.json"
|
|
104
|
+
value = source_path.read_text()
|
|
105
|
+
return cls.from_json(value)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_json(cls, value: str) -> ModelRegistry:
|
|
109
|
+
raw = json.loads(value)
|
|
110
|
+
if not isinstance(raw, dict):
|
|
111
|
+
raise RuntimeErrorBase("model registry must be a JSON object")
|
|
112
|
+
try:
|
|
113
|
+
definition = RegistryFile.model_validate(raw)
|
|
114
|
+
except ValueError as exc:
|
|
115
|
+
raise RuntimeErrorBase(f"invalid model registry: {exc}") from exc
|
|
116
|
+
if definition.default is not None and definition.default not in definition.models:
|
|
117
|
+
raise RuntimeErrorBase(f"registry default is not registered: {definition.default}")
|
|
118
|
+
for name, entry in definition.models.items():
|
|
119
|
+
if entry.recipe is not None and entry.recipe not in definition.recipes:
|
|
120
|
+
raise RuntimeErrorBase(
|
|
121
|
+
f"model {name!r} references unregistered recipe: {entry.recipe}"
|
|
122
|
+
)
|
|
123
|
+
return cls(definition)
|
|
124
|
+
|
|
125
|
+
def resolve(self, requested: str | None) -> tuple[str, RegistryModel]:
|
|
126
|
+
name = requested or self.definition.default
|
|
127
|
+
if name is None:
|
|
128
|
+
raise RuntimeErrorBase("request must specify model or registry needs a default")
|
|
129
|
+
entry = self.definition.models.get(name)
|
|
130
|
+
if entry is None:
|
|
131
|
+
raise RuntimeErrorBase(f"model is not registered: {name}")
|
|
132
|
+
if not entry.enabled:
|
|
133
|
+
raise RuntimeErrorBase(f"model is disabled: {name}")
|
|
134
|
+
return name, entry
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_transformers_runtime(
|
|
138
|
+
model_id: str,
|
|
139
|
+
config: dict[str, Any],
|
|
140
|
+
*,
|
|
141
|
+
batch_size_override: int | None = None,
|
|
142
|
+
) -> DecisionRuntime:
|
|
143
|
+
"""Select a Transformers readout from metadata, never from a model name."""
|
|
144
|
+
|
|
145
|
+
effective_config = dict(config)
|
|
146
|
+
if batch_size_override is not None:
|
|
147
|
+
if isinstance(batch_size_override, bool) or batch_size_override <= 0:
|
|
148
|
+
raise RuntimeErrorBase("model batch size override must be a positive integer")
|
|
149
|
+
# Dotted metadata has highest precedence in decision_metadata(), so this
|
|
150
|
+
# overrides both nested and dotted registry recipe values without
|
|
151
|
+
# mutating the registry's reusable configuration.
|
|
152
|
+
effective_config["decision.batch_size"] = batch_size_override
|
|
153
|
+
|
|
154
|
+
readout = decision_metadata(effective_config).get("readout")
|
|
155
|
+
if readout == "pointer_head":
|
|
156
|
+
return PointerTransformersBackend(model_id, config=effective_config)
|
|
157
|
+
if readout == "encoder_decoder_margin":
|
|
158
|
+
return EncoderDecoderMarginBackend(model_id, config=effective_config)
|
|
159
|
+
if readout == "sequence_classifier_margin":
|
|
160
|
+
return SequenceClassifierMarginBackend(model_id, config=effective_config)
|
|
161
|
+
if readout == "cross_encoder_margin":
|
|
162
|
+
return CrossEncoderBackend(model_id, config=effective_config)
|
|
163
|
+
if readout == "causal_options":
|
|
164
|
+
return CausalOptionsBackend(model_id, config=effective_config)
|
|
165
|
+
if readout == "bosun_decision_tokens":
|
|
166
|
+
return BosunDecisionBackend(model_id, config=effective_config)
|
|
167
|
+
if readout == "nli_entailment":
|
|
168
|
+
return NLIEntailmentBackend(model_id, config=effective_config)
|
|
169
|
+
if readout == "gliclass_calibrated":
|
|
170
|
+
return GLiClassCalibratedBackend(model_id, config=effective_config)
|
|
171
|
+
if readout == "openjev_scalar_head":
|
|
172
|
+
return OpenJevScalarHeadBackend(model_id, config=effective_config)
|
|
173
|
+
if readout == "semantic_option_head":
|
|
174
|
+
return SmallJevSemanticBackend(model_id, config=effective_config)
|
|
175
|
+
if readout == "hidden_state_probe":
|
|
176
|
+
return HiddenStateProbeBackend(model_id, config=effective_config)
|
|
177
|
+
if readout == "laya_native":
|
|
178
|
+
return LayaBackend(model_id, config=effective_config)
|
|
179
|
+
if readout == "diffusion_structured_read":
|
|
180
|
+
return DjevHTTPRuntime(model_id, config=effective_config)
|
|
181
|
+
if readout == "diffusion_thinking_read":
|
|
182
|
+
native_contract = decision_metadata(effective_config).get("native_contract")
|
|
183
|
+
if native_contract == "djev":
|
|
184
|
+
return DjevThinkingRuntime(model_id, config=effective_config)
|
|
185
|
+
if native_contract == "openjev":
|
|
186
|
+
return OpenJevThinkingHTTPRuntime(model_id, config=effective_config)
|
|
187
|
+
raise RuntimeErrorBase(
|
|
188
|
+
"diffusion_thinking_read requires decision.native_contract of 'djev' or 'openjev'"
|
|
189
|
+
)
|
|
190
|
+
if readout == "gliformer_native":
|
|
191
|
+
return JeffHTTPRuntime(model_id, config=effective_config)
|
|
192
|
+
if readout == "gliner2_multilabel":
|
|
193
|
+
return GLiNER2Runtime(model_id, config=effective_config)
|
|
194
|
+
return TransformersBackend(model_id, config=effective_config)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class RegistryRuntime(DecisionRuntime):
|
|
198
|
+
"""Lazy, cached runtime dispatch with per-model microbatching."""
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
registry: ModelRegistry,
|
|
203
|
+
*,
|
|
204
|
+
model_batch_size: int | None = None,
|
|
205
|
+
):
|
|
206
|
+
self.registry = registry
|
|
207
|
+
self.model_name = registry.definition.default or "registry"
|
|
208
|
+
self.model_batch_size = model_batch_size
|
|
209
|
+
self._runtimes: dict[str, DecisionRuntime] = {}
|
|
210
|
+
|
|
211
|
+
def _runtime(self, name: str, entry: RegistryModel) -> DecisionRuntime:
|
|
212
|
+
cached = self._runtimes.get(name)
|
|
213
|
+
if cached is not None:
|
|
214
|
+
return cached
|
|
215
|
+
recipe_config = (
|
|
216
|
+
self.registry.definition.recipes.get(entry.recipe, {})
|
|
217
|
+
if entry.recipe is not None
|
|
218
|
+
else {}
|
|
219
|
+
)
|
|
220
|
+
config = entry.resolved_config(recipe_config)
|
|
221
|
+
runtime: DecisionRuntime
|
|
222
|
+
if entry.backend == "llama":
|
|
223
|
+
if decision_metadata(config).get("readout") == "winnow_shared_branch":
|
|
224
|
+
runtime = WinnowHTTPRuntime(entry.model, config=config)
|
|
225
|
+
else:
|
|
226
|
+
runtime = LlamaBackend(entry.model, config=config)
|
|
227
|
+
elif entry.backend == "mlx":
|
|
228
|
+
raise RuntimeErrorBase(
|
|
229
|
+
"the MLX backend is registered but not installed in this service image"
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
runtime = build_transformers_runtime(
|
|
233
|
+
entry.model,
|
|
234
|
+
config,
|
|
235
|
+
batch_size_override=self.model_batch_size,
|
|
236
|
+
)
|
|
237
|
+
runtime = apply_question_type_support(runtime, config)
|
|
238
|
+
self._runtimes[name] = runtime
|
|
239
|
+
return runtime
|
|
240
|
+
|
|
241
|
+
def decide_batch(self, requests: Sequence[DecisionRequest]) -> list[DecisionResponse]:
|
|
242
|
+
grouped: dict[str, list[tuple[int, DecisionRequest]]] = defaultdict(list)
|
|
243
|
+
entries: dict[str, RegistryModel] = {}
|
|
244
|
+
for index, request in enumerate(requests):
|
|
245
|
+
name, entry = self.registry.resolve(request.model)
|
|
246
|
+
grouped[name].append((index, request))
|
|
247
|
+
entries[name] = entry
|
|
248
|
+
|
|
249
|
+
results: list[DecisionResponse | None] = [None] * len(requests)
|
|
250
|
+
for name, indexed_requests in grouped.items():
|
|
251
|
+
entry = entries[name]
|
|
252
|
+
recipe_config = (
|
|
253
|
+
self.registry.definition.recipes.get(entry.recipe, {})
|
|
254
|
+
if entry.recipe is not None
|
|
255
|
+
else {}
|
|
256
|
+
)
|
|
257
|
+
config = entry.resolved_config(recipe_config)
|
|
258
|
+
supported_types = configured_question_types(config)
|
|
259
|
+
supported_set = frozenset(supported_types)
|
|
260
|
+
if not any(
|
|
261
|
+
question.type in supported_set
|
|
262
|
+
for _, request in indexed_requests
|
|
263
|
+
for question in request.questions.values()
|
|
264
|
+
):
|
|
265
|
+
for index, request in indexed_requests:
|
|
266
|
+
results[index] = DecisionResponse(
|
|
267
|
+
model=entry.model,
|
|
268
|
+
answers={
|
|
269
|
+
question_name: UnsupportedAnswer(
|
|
270
|
+
type="unsupported",
|
|
271
|
+
question_type=question.type,
|
|
272
|
+
supported_types=list(supported_types),
|
|
273
|
+
)
|
|
274
|
+
for question_name, question in request.questions.items()
|
|
275
|
+
},
|
|
276
|
+
usage=Usage(),
|
|
277
|
+
)
|
|
278
|
+
continue
|
|
279
|
+
runtime = self._runtime(name, entries[name])
|
|
280
|
+
responses = runtime.decide_batch([request for _, request in indexed_requests])
|
|
281
|
+
for (index, _), response in zip(indexed_requests, responses, strict=True):
|
|
282
|
+
results[index] = response
|
|
283
|
+
return [result for result in results if result is not None]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Runtime interface and shared decision post-processing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from .protocol import (
|
|
11
|
+
ChoiceAnswer,
|
|
12
|
+
ChoiceQuestion,
|
|
13
|
+
DecisionRequest,
|
|
14
|
+
DecisionResponse,
|
|
15
|
+
NoulAnswer,
|
|
16
|
+
NoulQuestion,
|
|
17
|
+
QuestionType,
|
|
18
|
+
ScoreAnswer,
|
|
19
|
+
ScoreQuestion,
|
|
20
|
+
UnsupportedAnswer,
|
|
21
|
+
Usage,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RuntimeErrorBase(RuntimeError):
|
|
26
|
+
"""A user-facing runtime configuration or inference error."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def softmax(values: Sequence[float]) -> list[float]:
|
|
30
|
+
if not values:
|
|
31
|
+
raise RuntimeErrorBase("cannot normalize an empty score vector")
|
|
32
|
+
peak = max(values)
|
|
33
|
+
exps = [math.exp(value - peak) for value in values]
|
|
34
|
+
total = sum(exps)
|
|
35
|
+
return [value / total for value in exps]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DecisionRuntime(ABC):
|
|
39
|
+
"""Backend contract. Implementations may process the whole batch at once."""
|
|
40
|
+
|
|
41
|
+
model_name: str
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def decide_batch(
|
|
45
|
+
self, requests: Sequence[DecisionRequest]
|
|
46
|
+
) -> list[DecisionResponse]:
|
|
47
|
+
"""Evaluate requests, preserving input order."""
|
|
48
|
+
|
|
49
|
+
def decide(self, request: DecisionRequest) -> DecisionResponse:
|
|
50
|
+
return self.decide_batch([request])[0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
ALL_QUESTION_TYPES: tuple[QuestionType, ...] = ("choice", "score", "noul")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def configured_question_types(config: dict[str, Any]) -> tuple[QuestionType, ...]:
|
|
57
|
+
"""Read a model's declared capability boundary from decision metadata."""
|
|
58
|
+
|
|
59
|
+
nested = config.get("decision", {})
|
|
60
|
+
nested_types = nested.get("question_types") if isinstance(nested, dict) else None
|
|
61
|
+
value = config.get("decision.question_types", nested_types)
|
|
62
|
+
if value is None:
|
|
63
|
+
return ALL_QUESTION_TYPES
|
|
64
|
+
if not isinstance(value, list) or not value:
|
|
65
|
+
raise RuntimeErrorBase("decision.question_types must be a non-empty array")
|
|
66
|
+
if any(item not in ALL_QUESTION_TYPES for item in value):
|
|
67
|
+
raise RuntimeErrorBase(
|
|
68
|
+
"decision.question_types may contain only choice, score, and noul"
|
|
69
|
+
)
|
|
70
|
+
result: list[QuestionType] = []
|
|
71
|
+
for item in value:
|
|
72
|
+
typed_item = cast(QuestionType, item)
|
|
73
|
+
if typed_item not in result:
|
|
74
|
+
result.append(typed_item)
|
|
75
|
+
return tuple(result)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class QuestionTypeRuntime(DecisionRuntime):
|
|
79
|
+
"""Return explicit per-question unsupported results around any backend."""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
runtime: DecisionRuntime,
|
|
84
|
+
supported_types: Sequence[QuestionType],
|
|
85
|
+
) -> None:
|
|
86
|
+
self.runtime = runtime
|
|
87
|
+
self.model_name = runtime.model_name
|
|
88
|
+
self.supported_types = tuple(supported_types)
|
|
89
|
+
self._supported = frozenset(supported_types)
|
|
90
|
+
|
|
91
|
+
def decide_batch(
|
|
92
|
+
self, requests: Sequence[DecisionRequest]
|
|
93
|
+
) -> list[DecisionResponse]:
|
|
94
|
+
filtered: list[DecisionRequest] = []
|
|
95
|
+
filtered_indices: list[int] = []
|
|
96
|
+
for index, request in enumerate(requests):
|
|
97
|
+
questions = {
|
|
98
|
+
name: question
|
|
99
|
+
for name, question in request.questions.items()
|
|
100
|
+
if question.type in self._supported
|
|
101
|
+
}
|
|
102
|
+
if questions:
|
|
103
|
+
filtered.append(request.model_copy(update={"questions": questions}))
|
|
104
|
+
filtered_indices.append(index)
|
|
105
|
+
|
|
106
|
+
inferred: dict[int, DecisionResponse] = {}
|
|
107
|
+
if filtered:
|
|
108
|
+
responses = self.runtime.decide_batch(filtered)
|
|
109
|
+
if len(responses) != len(filtered):
|
|
110
|
+
raise RuntimeErrorBase("runtime returned the wrong batch length")
|
|
111
|
+
inferred = dict(zip(filtered_indices, responses, strict=True))
|
|
112
|
+
|
|
113
|
+
results: list[DecisionResponse] = []
|
|
114
|
+
for index, request in enumerate(requests):
|
|
115
|
+
response = inferred.get(index)
|
|
116
|
+
supported_answers = response.answers if response is not None else {}
|
|
117
|
+
answers: dict[str, Any] = {}
|
|
118
|
+
for name, question in request.questions.items():
|
|
119
|
+
if question.type in self._supported:
|
|
120
|
+
answer = supported_answers.get(name)
|
|
121
|
+
if answer is None:
|
|
122
|
+
raise RuntimeErrorBase(
|
|
123
|
+
f"runtime did not return answer for question {name!r}"
|
|
124
|
+
)
|
|
125
|
+
answers[name] = answer
|
|
126
|
+
else:
|
|
127
|
+
answers[name] = UnsupportedAnswer(
|
|
128
|
+
type="unsupported",
|
|
129
|
+
question_type=question.type,
|
|
130
|
+
supported_types=list(self.supported_types),
|
|
131
|
+
)
|
|
132
|
+
results.append(
|
|
133
|
+
DecisionResponse(
|
|
134
|
+
model=response.model if response is not None else self.model_name,
|
|
135
|
+
answers=answers,
|
|
136
|
+
usage=response.usage if response is not None else Usage(),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return results
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def apply_question_type_support(
|
|
143
|
+
runtime: DecisionRuntime, config: dict[str, Any]
|
|
144
|
+
) -> DecisionRuntime:
|
|
145
|
+
supported = configured_question_types(config)
|
|
146
|
+
if supported == ALL_QUESTION_TYPES:
|
|
147
|
+
return runtime
|
|
148
|
+
return QuestionTypeRuntime(runtime, supported)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TokenLogitRuntime(DecisionRuntime):
|
|
152
|
+
"""Shared formatter for backends that expose next-token logits."""
|
|
153
|
+
|
|
154
|
+
def __init__(self, *, model_name: str, config: dict[str, Any] | None = None):
|
|
155
|
+
self.model_name = model_name
|
|
156
|
+
self.config = config or {}
|
|
157
|
+
|
|
158
|
+
def _metadata(self, name: str, legacy_name: str) -> Any:
|
|
159
|
+
value = self.config.get(name, self.config.get(legacy_name))
|
|
160
|
+
if isinstance(value, dict) and name.startswith("decision."):
|
|
161
|
+
return value
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
def _prompt(self, request: DecisionRequest, question_name: str, question: Any) -> str:
|
|
165
|
+
template = self._metadata("decision.prompt_template", "prompt_template")
|
|
166
|
+
if not isinstance(template, str):
|
|
167
|
+
raise RuntimeErrorBase(
|
|
168
|
+
"model metadata must define a string decision prompt_template"
|
|
169
|
+
)
|
|
170
|
+
return template.format(
|
|
171
|
+
state=request.state,
|
|
172
|
+
question_name=question_name,
|
|
173
|
+
instructions=question.instructions,
|
|
174
|
+
criteria=question.criteria if hasattr(question, "criteria") else "",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _token_id(self, label: str) -> int:
|
|
178
|
+
tokens = self._metadata("decision.tokens", "decision_tokens")
|
|
179
|
+
if isinstance(tokens, str):
|
|
180
|
+
import json
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
tokens = json.loads(tokens)
|
|
184
|
+
except json.JSONDecodeError as exc:
|
|
185
|
+
raise RuntimeErrorBase("decision.tokens is not valid JSON") from exc
|
|
186
|
+
if not isinstance(tokens, dict) or label not in tokens:
|
|
187
|
+
raise RuntimeErrorBase(f"missing decision token mapping for {label!r}")
|
|
188
|
+
token_id = tokens[label]
|
|
189
|
+
if not isinstance(token_id, int):
|
|
190
|
+
raise RuntimeErrorBase(f"decision token for {label!r} is not an integer")
|
|
191
|
+
return token_id
|
|
192
|
+
|
|
193
|
+
def _answer(self, question: Any, logits: dict[int, float]) -> Any:
|
|
194
|
+
labels = self._labels_for_question(question)
|
|
195
|
+
return self.answer_from_label_scores(
|
|
196
|
+
question, {label: logits[self._token_id(label)] for label in labels}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _labels_for_question(question: Any) -> list[str]:
|
|
201
|
+
if isinstance(question, ChoiceQuestion):
|
|
202
|
+
return list(question.criteria)
|
|
203
|
+
if isinstance(question, ScoreQuestion):
|
|
204
|
+
return [str(index) for index in range(len(question.criteria))]
|
|
205
|
+
if isinstance(question, NoulQuestion):
|
|
206
|
+
return ["true", "false"]
|
|
207
|
+
raise RuntimeErrorBase(f"unsupported question type: {type(question).__name__}")
|
|
208
|
+
|
|
209
|
+
def answer_from_label_scores(self, question: Any, scores: dict[str, float]) -> Any:
|
|
210
|
+
if isinstance(question, ChoiceQuestion):
|
|
211
|
+
labels = list(question.criteria)
|
|
212
|
+
probabilities = softmax([scores[label] for label in labels])
|
|
213
|
+
distribution = dict(zip(labels, probabilities, strict=True))
|
|
214
|
+
choice = max(distribution, key=distribution.__getitem__)
|
|
215
|
+
return ChoiceAnswer(
|
|
216
|
+
type="choice",
|
|
217
|
+
choice=choice,
|
|
218
|
+
probabilities=distribution,
|
|
219
|
+
confidence=distribution[choice],
|
|
220
|
+
)
|
|
221
|
+
if isinstance(question, ScoreQuestion):
|
|
222
|
+
labels = [str(index) for index in range(len(question.criteria))]
|
|
223
|
+
probabilities = softmax([scores[label] for label in labels])
|
|
224
|
+
distribution = dict(zip(labels, probabilities, strict=True))
|
|
225
|
+
score = sum(index * probability for index, probability in enumerate(probabilities))
|
|
226
|
+
return ScoreAnswer(
|
|
227
|
+
type="score",
|
|
228
|
+
score=score,
|
|
229
|
+
probabilities=distribution,
|
|
230
|
+
confidence=max(probabilities),
|
|
231
|
+
legend=question.criteria,
|
|
232
|
+
)
|
|
233
|
+
if isinstance(question, NoulQuestion):
|
|
234
|
+
yes = scores["true"]
|
|
235
|
+
no = scores["false"]
|
|
236
|
+
return NoulAnswer(type="noul", noul=1.0 / (1.0 + math.exp(no - yes)))
|
|
237
|
+
raise RuntimeErrorBase(f"unsupported question type: {type(question).__name__}")
|