DEG-5B-E2B-1018 / source /tests /test_server.py
gcoderw's picture
Rename package to deg (DEG_ env vars, deg.json); note upcoming modalities
16d760d verified
Raw History Blame Contribute Delete
29.4 kB
"""Contract tests for the HTTP API (deg.api / deg.server / deg.client).
Golden fixtures:
* ``fixtures/openai_decisions_docs.json``: the OpenAI Decisions guide's requests and
illustrative responses (openai-python 3.26.1 types);
* ``fixtures/jev_systemone_live.json``: live api.typesafe.ai (jev-1.13.0) requests,
statuses and responses.
The model is :class:`fake_fill.FakeFill` (no torch); responses still go through
``deg.response.assemble`` and its schema validation.
"""
from __future__ import annotations
import json
import threading
import time
from pathlib import Path
from typing import Any
import pytest
from fake_fill import FakeFill
from deg.api import parse_decisions, parse_systemone
from deg.client import Deg, DegAPIError
from deg.response import MODEL_ID, confidence
from deg.server import Batcher, Engine, make_server
FIXTURES = Path(__file__).parent / "fixtures"
DOCS = json.loads((FIXTURES / "openai_decisions_docs.json").read_text())["cases"]
JEV = json.loads((FIXTURES / "jev_systemone_live.json").read_text())["cases"]
EVENT_INPUT = ("Hi, order AB-1234 arrived with a cracked screen on 2026-10-01. "
"I want a refund, not a replacement.")
EVENT_QUESTIONS = [
{"type": "choice", "name": "event", "instructions": "Which event should be dispatched?",
"choices": [{"value": "refund", "description": "Customer asks for money back."},
{"value": "replacement", "description": "Customer wants a new unit."},
{"value": "other"}]},
{"type": "predicate", "name": "damaged", "instructions": "cracked or broken item?"},
{"type": "extract", "name": "order_id", "instructions": "The order id."},
{"type": "extract", "name": "delivered", "instructions": "Delivery date.",
"value_type": "date"},
{"type": "extract", "name": "tracking", "instructions": "Tracking number.",
"pattern": "^1Z\\w+$"},
{"type": "generate", "name": "note", "instructions": "A one-line note for the agent.",
"max_length": 3, "max_tokens": 64},
]
class Server:
def __init__(self, *, api_key: str | None = None, fill: Any = None,
fill_many: Any = None, window_s: float = 0.001, max_pending: int = 256):
self.fill = fill or FakeFill()
self.batcher = Batcher(self.fill, fill_many=fill_many, window_s=window_s,
max_pending=max_pending)
self.httpd = make_server(Engine(self.batcher), "127.0.0.1", 0, api_key=api_key)
self.url = f"http://127.0.0.1:{self.httpd.server_address[1]}"
threading.Thread(target=self.httpd.serve_forever, daemon=True).start()
def close(self) -> None:
self.httpd.shutdown()
self.httpd.server_close()
@pytest.fixture(scope="module")
def server():
srv = Server()
yield srv
srv.close()
@pytest.fixture(scope="module")
def client(server):
return Deg(server.url)
def shape(value: Any) -> Any:
"""Keys (in order) and JSON types, recursively; lists by their first item."""
if isinstance(value, dict):
return {k: shape(v) for k, v in value.items()}
if isinstance(value, list):
return [shape(value[0])] if value else []
if isinstance(value, bool):
return "bool"
if isinstance(value, (int, float)):
return "number"
return type(value).__name__
def raw_post(url: str, path: str, body: Any, headers: dict | None = None) -> tuple[int, Any]:
import urllib.error
import urllib.request
data = body if isinstance(body, bytes) else json.dumps(body).encode()
req = urllib.request.Request(url + path, data=data, method="POST",
headers={"Content-Type": "application/json", **(headers or {})})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as exc:
return exc.code, json.loads(exc.read())
# ---------------------------------------------------------------- OpenAI Decisions
@pytest.mark.parametrize("case", ["choice", "score"])
def test_decisions_docs_golden_shape(client, case):
golden = DOCS[case]
got = client.decisions.create(**golden["request"])
excerpt = golden["response_excerpt"]
assert shape(got["answers"]) == shape(excerpt["answers"])
assert list(got) == ["answers", "model", "usage"]
assert got.model == MODEL_ID
usage = got.usage
assert list(usage) == ["input_tokens", "input_tokens_details", "output_tokens",
"output_tokens_details", "total_tokens"]
assert usage.total_tokens == usage.input_tokens + usage.output_tokens > 0
assert usage.output_tokens == 0 # decisions generate nothing
ans = got.answers[0]
probs = [p.probability for p in ans.probabilities]
assert abs(sum(probs) - 1) < 1e-9
assert ans.confidence == pytest.approx(confidence(probs))
if case == "choice":
assert [p.value for p in ans.probabilities] == [
c["value"] for c in golden["request"]["questions"][0]["choices"]]
assert ans.choice == max(ans.probabilities, key=lambda p: p.probability).value
else:
assert [(p.value, p.label) for p in ans.probabilities] == [
(0, "Cosmetic"), (1, "Workaround available"), (2, "Fully blocked")]
assert ans.score == pytest.approx(sum(p.value * p.probability
for p in ans.probabilities))
def test_decisions_docs_score_formula():
"""The guide's own numbers: probabilities .1/.7/.2 over indices 0/1/2 -> score 1.1."""
from deg.api import _expected
assert _expected([0.1, 0.7, 0.2]) == pytest.approx(1.1)
def test_decisions_predicate_text_and_messages(client):
req = json.loads(json.dumps(DOCS["predicate_image"]["request"]))
req["input"][0]["content"] = [{"type": "input_text", "text": "Does the box show damage?"}]
got = client.decisions.create(**req)
assert shape(got.answers) == shape(DOCS["predicate_image"]["response_excerpt"]["answers"])
assert 0 <= got.answers[0].probability <= 1
def test_decisions_image_refused(server):
status, body = raw_post(server.url, "/v1/decisions", DOCS["predicate_image"]["request"])
assert status == 400
assert body["error"]["type"] == "invalid_request_error"
assert "input_image is not supported" in body["error"]["message"]
def test_decisions_extensions_event_dispatch(server, client):
got = client.decisions.create(model="deg-latest", input=EVENT_INPUT,
questions=EVENT_QUESTIONS)
a = {x.name: x for x in got.answers}
assert [x.name for x in got.answers] == [q["name"] for q in EVENT_QUESTIONS]
assert a["event"].type == "choice" and a["event"].choice == "refund"
assert a["damaged"].type == "predicate"
assert a["order_id"] == {"name": "order_id", "type": "extract", "value": "AB-1234",
"span": [10, 17], "confidence": 0.9}
assert EVENT_INPUT[10:17] == "AB-1234"
assert a["delivered"].value == "2026-10-01"
assert a["tracking"].value is None and a["tracking"].null_reason == "not_found"
assert a["tracking"].span is None
assert a["note"] == {"name": "note", "type": "generate", "text": "not",
"output_tokens": 2}
assert got.usage.output_tokens == 2
# one fill for the whole request; extensions compiled to the documented schema
_, schema, options = server.fill.calls[-1]
assert list(schema["properties"]) == [q["name"] for q in EVENT_QUESTIONS]
assert schema["properties"]["note"] == {"type": "string", "x-generate": True,
"description": "A one-line note for the agent.",
"maxLength": 3, "x-max-tokens": 64}
assert schema["properties"]["delivered"]["format"] == "date"
assert schema["properties"]["order_id"]["type"] == ["string", "null"]
assert options.max_new_tokens == 256 # request default; the note has its own budget
def test_decisions_score_levels_go_to_x_levels(server, client):
got = client.decisions.create(model="deg-latest", input="I am furious", questions=[
{"type": "score", "name": "anger", "instructions": "How angry is the customer?",
"levels": [{"label": "calm", "description": "no complaint"}, {"label": "annoyed"},
{"label": "furious", "description": "threatens to leave"}]}])
_, schema, _ = server.fill.calls[-1]
assert schema["properties"]["anger"] == {
"type": "integer", "minimum": 0, "maximum": 2,
"description": "How angry is the customer?",
"x-levels": {"0": "calm: no complaint", "1": "annoyed",
"2": "furious: threatens to leave"}}
assert [p.label for p in got.answers[0].probabilities] == ["calm", "annoyed", "furious"]
def test_generate_questions_mix_max_tokens(server, client):
got = client.decisions.create(model="deg-latest", input="refund please", questions=[
{"type": "generate", "name": "a", "instructions": "x", "max_tokens": 8},
{"type": "generate", "name": "b", "instructions": "y", "max_tokens": 9},
{"type": "generate", "name": "c", "instructions": "z"}])
assert [a.type for a in got.answers] == ["generate"] * 3
_, schema, options = server.fill.calls[-1]
props = schema["properties"]
assert props["a"]["x-max-tokens"] == 8 and props["b"]["x-max-tokens"] == 9
assert "x-max-tokens" not in props["c"] and options.max_new_tokens == 256
status, _ = raw_post(server.url, "/v1/systemone", {
"model": "jev-latest", "state": "refund please", "questions": {
"a": {"type": "generate", "instructions": "x", "max_tokens": 4},
"b": {"type": "generate", "instructions": "y", "max_tokens": 12}}})
assert status == 200
_, schema, _ = server.fill.calls[-1]
assert [schema["properties"][k]["x-max-tokens"] for k in "ab"] == [4, 12]
def test_decisions_unnamed_questions_get_null_name(client):
got = client.decisions.create(model="deg-latest", input="refund please", questions=[
{"type": "predicate", "instructions": "refund requested?"},
{"type": "predicate", "instructions": "angry?"}])
assert [a.name for a in got.answers] == [None, None]
def test_decisions_single_choice_and_level_without_model(server, client):
before = len(server.fill.calls)
got = client.decisions.create(model="deg-latest", input="x", questions=[
{"type": "choice", "name": "c", "instructions": "pick", "choices": [{"value": True}]},
{"type": "score", "name": "s", "instructions": "rate", "levels": [{"label": "only"}]}])
assert len(server.fill.calls) == before
assert got.answers[0].choice is True and got.answers[0].confidence == 1.0
assert got.answers[1].score == 0 and got.usage.input_tokens == 0
@pytest.mark.parametrize("questions,needle", [
([{"type": "classify", "instructions": "x"}], "type must be one of"),
([{"type": "choice", "instructions": "x",
"choices": [{"value": f"o{i}"} for i in range(256)]}], "Too many choices"),
([{"type": "score", "instructions": "x",
"levels": [{"label": str(i)} for i in range(11)]}], "Too many score levels"),
([{"type": "score", "instructions": "x", "levels": []}], "levels must be a non-empty"),
([{"type": "choice", "instructions": "x", "choices": []}], "choices must be a non-empty"),
([{"type": "choice", "instructions": "x",
"choices": [{"value": "a"}, {"value": "a"}]}], "duplicate choice value"),
([{"type": "predicate", "name": "a", "instructions": "x"},
{"type": "predicate", "name": "a", "instructions": "y"}], "names must be unique"),
([{"type": "predicate", "instructions": "x", "choices": []}], "Unrecognized question"),
([{"type": "generate", "instructions": "x", "max_tokens": 0}], "max_tokens must be"),
([{"type": "generate", "instructions": "x", "pattern": "("}], "pattern"),
([{"type": "extract", "instructions": "x", "value_type": "date",
"max_length": 3}], "applies only to string"),
([{"type": "extract", "instructions": "x", "value_type": "phone"}], "value_type"),
([], "non-empty array"),
])
def test_decisions_validation_errors(server, questions, needle):
status, body = raw_post(server.url, "/v1/decisions",
{"model": "gpt-6-luna", "input": "x", "questions": questions})
assert status == 400, body
assert set(body["error"]) == {"message", "type", "param", "code"}
assert needle in body["error"]["message"]
def test_decisions_choice_values_are_typed(client):
got = client.decisions.create(model="deg-latest", input="true", questions=[
{"type": "choice", "name": "c", "instructions": "pick",
"choices": [{"value": "true"}, {"value": True}]}])
assert [p.value for p in got.answers[0].probabilities] == ["true", True]
def test_decisions_unknown_model_and_fields(server):
status, body = raw_post(server.url, "/v1/decisions", {
"model": "gpt-5", "input": "x", "questions": [{"type": "predicate",
"instructions": "y"}], "foo": 1})
assert status == 400
msg = body["error"]["message"]
assert "Unrecognized request argument supplied: foo" in msg
assert "The model `gpt-5` does not exist" in msg
def test_decisions_request_options(server, client):
with pytest.raises(DegAPIError) as exc:
client.decisions.create(model="deg-latest", input="x", options={"temperature": 0.5},
questions=[{"type": "generate", "instructions": "y"}])
assert exc.value.status == 400 and "requires a seed" in exc.value.body["error"]["message"]
client.decisions.create(model="deg-latest", input="x",
options={"temperature": 0.5, "seed": 3},
questions=[{"type": "generate", "instructions": "y"}])
assert server.fill.calls[-1][2].seed == 3
# ---------------------------------------------------------------- stock openai SDK
def test_stock_openai_sdk_round_trip(server):
openai = pytest.importorskip("openai")
from openai.types.decision import (
AnswerAnswerResourceChoice,
AnswerAnswerResourcePredicate,
AnswerAnswerResourceScore,
Decision,
)
oai = openai.OpenAI(base_url=server.url + "/v1", api_key="unused", max_retries=0)
for case in ("choice", "score"):
got = oai.decisions.create(**DOCS[case]["request"])
assert isinstance(got, Decision)
want = {"choice": AnswerAnswerResourceChoice, "score": AnswerAnswerResourceScore}[case]
assert isinstance(got.answers[0], want)
assert got.answers[0].name == DOCS[case]["request"]["questions"][0]["name"]
assert got.usage.total_tokens > 0
got = oai.decisions.create(model="gpt-6-luna", input=[
{"role": "user", "content": [{"type": "input_text", "text": "cracked lid"}]}],
questions=[{"type": "predicate", "name": "visible_damage",
"instructions": "cracked anything?"}])
assert isinstance(got.answers[0], AnswerAnswerResourcePredicate)
assert got.answers[0].probability == pytest.approx(0.8)
def test_stock_openai_sdk_extensions_and_errors(server):
openai = pytest.importorskip("openai")
oai = openai.OpenAI(base_url=server.url + "/v1", api_key="unused", max_retries=0)
got = oai.decisions.create(model="deg-latest", input=EVENT_INPUT,
questions=EVENT_QUESTIONS,
extra_body={"options": {"date_order": "MDY"}})
by_name = {a.name: a for a in got.answers}
assert by_name["event"].choice == "refund"
# extension answers are not in the SDK's union; their fields stay readable
assert by_name["order_id"].type == "extract"
assert by_name["order_id"].value == "AB-1234"
assert by_name["note"].text == "not"
assert server.fill.calls[-1][2].date_order == "MDY"
with pytest.raises(openai.BadRequestError) as exc:
oai.decisions.create(model="gpt-6-luna", input="x",
questions=[{"type": "score", "instructions": "y",
"levels": [{"label": str(i)} for i in range(11)]}])
assert "Too many score levels" in str(exc.value)
def test_stock_openai_sdk_auth():
openai = pytest.importorskip("openai")
srv = Server(api_key="k1")
try:
ok = openai.OpenAI(base_url=srv.url + "/v1", api_key="k1", max_retries=0)
assert ok.decisions.create(**DOCS["choice"]["request"]).answers[0].type == "choice"
bad = openai.OpenAI(base_url=srv.url + "/v1", api_key="nope", max_retries=0)
with pytest.raises(openai.AuthenticationError):
bad.decisions.create(**DOCS["choice"]["request"])
status, body = raw_post(srv.url, "/v1/systemone", JEV["ok_all"]["request"])
assert status == 403
assert body == {"detail": {"error_type": "authentication_error", "message":
"Must supply an API key! Check your request and try again."}}
finally:
srv.close()
# ---------------------------------------------------------------- Jev SystemOne
@pytest.mark.parametrize("case", ["ok_all", "uncertain", "obj_state", "extra_q_name",
"one_option", "one_level"])
def test_systemone_live_shapes(server, case):
live = JEV[case]
status, body = raw_post(server.url, "/v1/systemone", live["request"])
assert status == live["status"] == 200
assert shape(body) == shape(live["response"]) # keys in the same order, same types
assert list(body["answers"]) == list(live["request"]["questions"])
for qid, ans in body["answers"].items():
if ans["type"] in ("choice", "score"):
probs = list(ans["probabilities"].values())
assert abs(sum(probs) - 1) < 1e-9
assert ans["confidence"] == pytest.approx(confidence(probs) if len(probs) > 1
else 1.0)
if ans["type"] == "score":
assert ans["legend"] == live["response"]["answers"][qid]["legend"]
assert ans["score"] == pytest.approx(sum(int(k) * v for k, v in
ans["probabilities"].items()))
assert body["model"] == MODEL_ID
@pytest.mark.parametrize("case", ["unknown_type", "extra_top", "bad_model", "opts256",
"eleven_levels", "no_model", "no_questions"])
def test_systemone_live_errors(server, case):
live = JEV[case]
request = json.loads(json.dumps(live["request"]))
# deg accepts two things live Jev rejects (opt-in extensions): the question type
# `extract` and the top-level `options`; probe the same rule with other names.
if case == "unknown_type":
request["questions"]["x"]["type"] = "classify"
if case == "extra_top":
request["foo"] = request.pop("options")
status, body = raw_post(server.url, "/v1/systemone", request)
assert status == live["status"]
if case == "bad_model":
assert body == live["response"]
elif status == 400:
assert body == live["response"]
else: # 422: the same pydantic-style item (type, loc, msg)
got = [{k: d[k] for k in ("type", "loc", "msg")} for d in body["detail"]]
want = [{k: d[k] for k in ("type", "loc", "msg")} for d in live["response"]["detail"]]
assert got == want
def test_systemone_noul_criteria_and_score_levels_in_schema(server):
raw_post(server.url, "/v1/systemone", JEV["ok_all"]["request"])
_, schema, _ = server.fill.calls[-1]
props = schema["properties"]
assert props["urgent_crit"]["description"] == (
"Is the customer asking for urgent action?\nYes means: Asks for action now\n"
"No means: No time pressure")
assert props["anger"] == {"type": "integer", "minimum": 0, "maximum": 3,
"description": "How angry is the customer?",
"x-levels": {"0": "calm", "1": "annoyed", "2": "angry",
"3": "furious"}}
assert props["department"]["oneOf"][2] == {"const": "shipping"}
def test_systemone_extensions(client):
got = client.systemone(EVENT_INPUT, {
"event": {"type": "choice", "instructions": "Which event?",
"criteria": {"refund": "money back", "replacement": None}},
"order_id": {"type": "extract", "instructions": "The order id."},
"note": {"type": "generate", "instructions": "Note for the agent.", "max_length": 4}})
assert got.answers["event"].choice == "refund"
assert got.answers["order_id"] == {"type": "extract", "value": "AB-1234", "span": [10, 17],
"confidence": 0.9}
assert got.answers["note"] == {"type": "generate", "text": "note", "output_tokens": 2}
assert got.usage == {"input_tokens": got.usage.input_tokens, "output_tokens": 2}
def test_systemone_existing_jev_client_unchanged(server):
"""The Clef/SystemOne client (rwkv_omarchy.rl.clef_server.ClefJudge, model
"clef-flash") works against deg as-is."""
clef = pytest.importorskip("rwkv_omarchy.rl.clef_server")
QUESTIONS, ClefJudge, build_request = clef.QUESTIONS, clef.ClefJudge, clef.build_request
judge = ClefJudge(server.url)
out = judge.judge_request(build_request("fix dns", [{"command": "ls", "output": "x"}],
"Done, DNS fixed."))
assert set(out) >= set(QUESTIONS) and out["input_tokens"] > 0
# ---------------------------------------------------------------- /v1/fill
def test_fill_round_trip_and_x_generate_on_enum(client):
schema = {"type": "object", "properties": {
"event": {"enum": ["refund", "replacement"]},
"order_id": {"type": "string"},
"note": {"type": "string", "x-generate": True, "maxLength": 4}},
"required": ["event", "order_id", "note"]}
got = client.fill(EVENT_INPUT, schema)
assert got.value == {"event": "refund", "order_id": "AB-1234", "note": "note"}
assert got.meta["/order_id"].span == [10, 17]
assert got.usage.generated_tokens == 2
bad = json.loads(json.dumps(schema))
bad["properties"]["event"]["x-generate"] = True
with pytest.raises(DegAPIError) as exc:
client.fill(EVENT_INPUT, bad)
assert exc.value.status == 422
assert exc.value.body["error"]["type"] == "invalid_request"
assert any("x-generate" in d["msg"] for d in exc.value.body["error"]["detail"])
def test_fill_unknown_option_is_422(client):
with pytest.raises(DegAPIError) as exc:
client.fill("x", {"type": "object", "properties": {"a": {"type": "boolean"}}},
options={"beam": 4})
assert exc.value.status == 422
def test_malformed_json_bodies(server):
assert raw_post(server.url, "/v1/decisions", b"{")[0] == 400
assert raw_post(server.url, "/v1/systemone", b"{")[0] == 422
assert raw_post(server.url, "/v1/fill", b"{")[0] == 422
assert raw_post(server.url, "/v1/nope", {})[0] == 404
def test_models_and_health(client):
ids = [m.id for m in client.models().data]
assert {"deg-latest", MODEL_ID, "gpt-6-luna", "jev-latest"} <= set(ids)
health = client.health()
assert health.ok and health.model == MODEL_ID and health.batcher["jobs"] >= 0
# ---------------------------------------------------------------- batching
def test_batcher_groups_concurrent_jobs_and_isolates_failures():
seen: list[int] = []
fake = FakeFill()
def fill(ctx, schema, opts):
if ctx == "bad":
raise ValueError("boom")
return fake(ctx, schema, opts)
def fill_many(jobs):
seen.append(len(jobs))
time.sleep(0.05)
return [ValueError("boom") if ctx == "bad" else fake(ctx, schema, opts)
for ctx, schema, opts in jobs]
srv = Server(fill=fill, fill_many=fill_many, window_s=0.2)
try:
req = {"model": "deg-latest", "questions": [{"type": "predicate",
"instructions": "refund?"}]}
results: dict[int, tuple[int, Any]] = {}
def go(i: int) -> None:
results[i] = raw_post(srv.url, "/v1/decisions",
dict(req, input="bad" if i == 3 else f"refund {i}"))
threads = [threading.Thread(target=go, args=(i,)) for i in range(6)]
for t in threads:
t.start()
for t in threads:
t.join()
assert max(seen) > 1
assert srv.batcher.stats()["max_seen_batch"] > 1
assert results[3][0] == 500 and results[3][1]["error"]["type"] == "server_error"
assert all(results[i][0] == 200 for i in range(6) if i != 3)
finally:
srv.close()
def test_server_fill_many_equals_sequential_fill():
"""The real runtime bound like ``--ckpt`` (``bind_model``) on a tiny random Qwen3.5:
concurrent requests batched through ``fill_many`` answer exactly as one-at-a-time
``fill`` (SPEC.md §10)."""
torch = pytest.importorskip("torch")
pytest.importorskip("transformers")
from test_model import SNAPSHOT, tiny_lm
from deg.model import load_model, load_tokenizer
from deg.server import bind_model
if SNAPSHOT is None:
pytest.skip("Qwen3.5-0.8B tokenizer not cached")
model = load_model(None, device="cpu", dtype=torch.float32, lm=tiny_lm(),
tokenizer=load_tokenizer(SNAPSHOT))
torch.manual_seed(1)
with torch.no_grad():
for p in model.heads.parameters():
p.normal_(0.0, 0.5)
fill, fill_many = bind_model(model)
assert fill_many is not None
calls: list[int] = []
def counted_many(jobs):
calls.append(len(jobs))
return fill_many(jobs)
reqs = [
("/v1/decisions", {"model": "deg-latest", "input": EVENT_INPUT, "questions": [
*EVENT_QUESTIONS[:4],
{"type": "score", "name": "anger", "instructions": "How angry?",
"levels": [{"label": "calm"}, {"label": "angry", "description": "upset"}]},
{"type": "generate", "name": "n1", "instructions": "Short note.", "max_tokens": 3},
{"type": "generate", "name": "n2", "instructions": "Reply.", "max_tokens": 5}]}),
("/v1/systemone", {"model": "jev-latest", "state": {"msg": "late parcel, angry"},
"questions": {"anger": {"type": "score", "instructions": "anger",
"criteria": ["calm", "angry", "furious"]},
"late": {"type": "noul", "instructions": "late?"}}}),
("/v1/decisions", {"model": "deg-latest", "input": "Order AB-9 for $80.",
"questions": [EVENT_QUESTIONS[0], EVENT_QUESTIONS[2]]}),
]
seq = Server(fill=fill, fill_many=None)
try:
want = [raw_post(seq.url, path, body) for path, body in reqs]
finally:
seq.close()
assert all(status == 200 for status, _ in want), want
srv = Server(fill=fill, fill_many=counted_many, window_s=0.5)
try:
got: dict[int, Any] = {}
threads = [threading.Thread(target=lambda i=i: got.__setitem__(i, raw_post(
srv.url, *reqs[i]))) for i in range(len(reqs))]
for t in threads:
t.start()
for t in threads:
t.join()
finally:
srv.close()
assert calls and max(calls) > 1
assert [got[i] for i in range(len(reqs))] == want
def test_batcher_rejects_when_full():
gate = threading.Event()
def slow(ctx, schema, opts):
gate.wait(5)
return FakeFill()(ctx, schema, opts)
srv = Server(fill=slow, max_pending=1)
try:
req = {"model": "deg-latest", "input": "x",
"questions": [{"type": "predicate", "instructions": "y"}]}
out: list = []
threads = [threading.Thread(target=lambda: out.append(
raw_post(srv.url, "/v1/decisions", req))) for _ in range(4)]
for t in threads:
t.start()
time.sleep(0.5)
gate.set()
for t in threads:
t.join()
codes = sorted(c for c, _ in out)
assert 429 in codes and 200 in codes
body = next(b for c, b in out if c == 429)
assert body["error"]["code"] == "rate_limit_exceeded"
finally:
srv.close()
# ---------------------------------------------------------------- pure parsing
def test_parse_is_shared_between_apis():
"""predicate == noul, choice == choice, score == score: same field schemas."""
o = parse_decisions({"model": "deg-latest", "input": "s", "questions": [
{"type": "predicate", "name": "a", "instructions": "i"},
{"type": "choice", "name": "b", "instructions": "i",
"choices": [{"value": "x", "description": "dx"}, {"value": "y"}]},
{"type": "score", "name": "c", "instructions": "i",
"levels": [{"label": "lo"}, {"label": "hi"}]}]})
j = parse_systemone({"model": "deg-latest", "state": "s", "questions": {
"a": {"type": "noul", "instructions": "i"},
"b": {"type": "choice", "instructions": "i", "criteria": {"x": "dx", "y": None}},
"c": {"type": "score", "instructions": "i", "criteria": ["lo", "hi"]}}})
assert o.schema == j.schema