feat: additional annotation control types and gating

- Introduces unified ControlNet/annotation type constants and extends API models/parsers to support `annotation` forms, `annotation_types`, and `allow_extended_controlnet`.
- Dispatch logic now gates extended control types by bridge capability/version and worker opt-in, while preserving legacy `hough` behavior.
- Alchemy annotation requests now validate `payload.control_type`, return image-output handling via R2, and use detector-class kudos buckets for pricing.
- Worker persistence/details were extended for advertised annotation types,
- Unit/integration coverage for relevant behavior was added, including a cross-repo SDK wire-contract test for request/response compatibility.

fix: include sql migration commands
tests: reconcile annotator tests with torch->numpy change
This commit is contained in:
tazlin committed 2026-08-19 14:02:10 -04:00
1 parent fbd5ed465e
commit f57a799de9
21 files changed
+1410 -25

No files matched your search

+60 -16
View File
@@ -7,7 +7,7 @@ import copy
from flask_restx import fields
from horde.apis.models import v2
from horde.consts import KNOWN_POST_PROCESSORS, KNOWN_SAMPLERS, KNOWN_WORKFLOWS
from horde.consts import IMAGE_CONTROL_TYPES, KNOWN_CONTROL_TYPES, KNOWN_POST_PROCESSORS, KNOWN_SAMPLERS, KNOWN_WORKFLOWS
from horde.vars import horde_title
@@ -138,6 +138,14 @@ class ImageParsers(v2.Parsers):
help="If True, this worker will pick up requests requesting ControlNet.",
location="json",
)
self.job_pop_parser.add_argument(
"allow_extended_controlnet",
type=bool,
required=False,
default=False,
help="If True, this worker will pick up requests requesting extended ControlNet control types.",
location="json",
)
self.job_pop_parser.add_argument(
"allow_sdxl_controlnet",
type=bool,
@@ -416,17 +424,7 @@ class ImageModels(v2.Models):
),
"control_type": fields.String(
required=False,
enum=[
"canny",
"hed",
"depth",
"normal",
"openpose",
"seg",
"scribble",
"fakescribbles",
"hough",
],
enum=IMAGE_CONTROL_TYPES,
),
"image_is_control": fields.Boolean(
default=False,
@@ -566,6 +564,10 @@ class ImageModels(v2.Models):
default=True,
description="If True, this worker will pick up requests requesting ControlNet.",
),
"allow_extended_controlnet": fields.Boolean(
default=False,
description="If True, this worker will pick up requests requesting extended ControlNet control types.",
),
"allow_sdxl_controlnet": fields.Boolean(
default=True,
description="If True, this worker will pick up requests requesting SDXL ControlNet.",
@@ -745,14 +747,30 @@ class ImageModels(v2.Models):
# Intentionally left blank to allow to add payloads later
self.input_model_interrogation_form_payload = api.model(
"ModelInterrogationFormPayloadStable",
{"*": fields.Wildcard(fields.String)},
{
"control_type": fields.String(
required=False,
enum=KNOWN_CONTROL_TYPES,
description="For the 'annotation' form, the controlnet control map to produce.",
),
"*": fields.Wildcard(fields.String),
},
)
self.input_model_interrogation_form = api.model(
"ModelInterrogationFormStable",
{
"name": fields.String(
required=True,
enum=["caption", "interrogation", "nsfw", "vectorize", "palette", "describe", "aesthetic"]
enum=[
"caption",
"interrogation",
"nsfw",
"vectorize",
"palette",
"describe",
"aesthetic",
"annotation",
]
+ list(KNOWN_POST_PROCESSORS.keys()),
description="The type of interrogation this is.",
unique=True,
@@ -832,11 +850,28 @@ class ImageModels(v2.Models):
"forms": fields.List(
fields.String(
description="The type of interrogation this worker can fulfil.",
enum=["caption", "interrogation", "nsfw", "vectorize", "palette", "describe", "aesthetic"]
enum=[
"caption",
"interrogation",
"nsfw",
"vectorize",
"palette",
"describe",
"aesthetic",
"annotation",
]
+ list(KNOWN_POST_PROCESSORS.keys()),
unique=True,
),
),
"annotation_types": fields.List(
fields.String(
description="The annotation control types this worker can fulfil.",
enum=KNOWN_CONTROL_TYPES,
),
required=False,
unique=True,
),
"amount": fields.Integer(
default=1,
description="The amount of forms to pop at the same time.",
@@ -872,7 +907,16 @@ class ImageModels(v2.Models):
),
"form": fields.String(
description="The name of this interrogation form",
enum=["caption", "interrogation", "nsfw", "vectorize", "palette", "describe", "aesthetic"]
enum=[
"caption",
"interrogation",
"nsfw",
"vectorize",
"palette",
"describe",
"aesthetic",
"annotation",
]
+ list(KNOWN_POST_PROCESSORS.keys()),
),
"payload": fields.Nested(self.input_model_interrogation_form_payload, skip_none=True),
+7
View File
@@ -866,6 +866,9 @@ class Models:
),
"models": fields.List(fields.String(description="Which models this worker if offering.")),
"forms": fields.List(fields.String(description="Which forms this worker if offering.")),
"annotation_types": fields.List(
fields.String(description="Which annotation control types this worker is offering."),
),
"team": fields.Nested(
self.response_model_team_details_lite,
"The Team to which this worker is dedicated.",
@@ -910,6 +913,10 @@ class Models:
default=None,
description="If True, this worker supports and allows controlnet requests.",
),
"extended_controlnet": fields.Boolean(
default=None,
description="If True, this worker supports and allows extended controlnet control types.",
),
"sdxl_controlnet": fields.Boolean(
default=None,
description="If True, this worker supports and allows SDXL controlnet requests.",
+34
View File
@@ -35,6 +35,7 @@ from horde.classes.stable.interrogation import Interrogation
from horde.classes.stable.interrogation_worker import InterrogationWorker
from horde.classes.stable.waiting_prompt import ImageWaitingPrompt
from horde.classes.stable.worker import ImageWorker
from horde.consts import KNOWN_CONTROL_TYPES
from horde.countermeasures import CounterMeasures
from horde.database import functions as database
from horde.database.kudos_reservations import reserve_kudos
@@ -710,6 +711,7 @@ class ImageJobPop(JobPopTemplate):
allow_unsafe_ipaddr=self.args.allow_unsafe_ipaddr,
allow_post_processing=self.args.allow_post_processing,
allow_controlnet=self.args.allow_controlnet,
allow_extended_controlnet=self.args.allow_extended_controlnet,
allow_sdxl_controlnet=self.args.allow_sdxl_controlnet,
allow_lora=self.args.allow_lora,
extra_slow_worker=self.args.extra_slow_worker,
@@ -1028,6 +1030,15 @@ class Interrogate(Resource):
def validate(self):
if settings.mode_maintenance():
raise e.MaintenanceMode("Interrogate")
for form in self.forms:
if form.get("name") != "annotation":
continue
control_type = (form.get("payload") or {}).get("control_type")
if control_type not in KNOWN_CONTROL_TYPES:
raise e.BadRequest(
f"The 'annotation' form requires a payload.control_type from {KNOWN_CONTROL_TYPES}.",
rc="InvalidControlType",
)
if self.args.webhook and not self.args.webhook.startswith("https://"):
raise e.BadRequest("webhooks need to point to an https endpoint.")
with get_app().app_context():
@@ -1189,6 +1200,14 @@ class InterrogatePop(JobPopTemplate):
help="The maximum amount of 512x512 tiles this worker can post-process",
location="json",
)
post_parser.add_argument(
"annotation_types",
type=list,
required=False,
default=None,
help="The annotation control types this worker can fulfil",
location="json",
)
decorators = [limiter.limit("60/second")]
@@ -1223,6 +1242,7 @@ class InterrogatePop(JobPopTemplate):
self.forms = []
if self.args.forms:
self.forms = self.args.forms
self.annotation_types = set(self.args.annotation_types or [])
self.worker_ip = request.remote_addr
self.validate()
self.check_in()
@@ -1247,6 +1267,8 @@ class InterrogatePop(JobPopTemplate):
priority_user_ids=self.priority_user_ids,
)
for form in priority_list:
if not self._supports_annotation_type(form):
continue
# We append to the list so that we have the prioritized forms first
self.prioritized_forms.append(form)
@@ -1257,6 +1279,8 @@ class InterrogatePop(JobPopTemplate):
forms_list=self.forms,
excluded_forms=self.prioritized_forms,
):
if not self._supports_annotation_type(form):
continue
self.prioritized_forms.append(form)
# logger.warning(datetime.utcnow())
worker_ret = {"forms": []}
@@ -1299,10 +1323,20 @@ class InterrogatePop(JobPopTemplate):
# logger.warning(datetime.utcnow())
return ({"skipped": self.skipped}, 200)
def _supports_annotation_type(self, form):
"""Return whether this worker advertises the annotation type requested by ``form``."""
if form.name != "annotation":
return True
if not self.annotation_types:
return False
payload = form.payload or {}
return payload.get("control_type") in self.annotation_types
def check_in(self):
self.worker.check_in(
max_tiles=self.args.max_tiles,
forms=self.forms,
annotation_types=self.annotation_types,
safe_ip=self.safe_ip,
ipaddr=self.worker_ip,
threads=self.args.threads,
+5
View File
@@ -9,8 +9,13 @@ import semver
from horde.consts import KNOWN_POST_PROCESSORS
from horde.logger import logger
# Bridge version of "AI Horde Worker reGen" from which the image-utilities backend can annotate the
# extended controlnet control types (everything outside LEGACY_IMAGE_CONTROL_TYPES).
EXTENDED_CONTROLNET_REGEN_VERSION = 17
BRIDGE_CAPABILITIES = {
"AI Horde Worker reGen": {
EXTENDED_CONTROLNET_REGEN_VERSION: {"extended_controlnet"},
13: {
"4xNomos8kSC",
"4xLSDIRplus",
+13 -5
View File
@@ -13,7 +13,7 @@ from sqlalchemy import JSON, Enum
from sqlalchemy.dialects.postgresql import JSONB, UUID
from horde.classes.base.kudos import kudos_event
from horde.consts import KNOWN_POST_PROCESSORS
from horde.consts import IMAGE_RESULT_ALCHEMY_FORMS, annotation_detector_kudos_bucket
from horde.database.kudos_reservations import release_reservation, reserve_kudos
from horde.enums import State
from horde.flask import SQLITE_MODE, db
@@ -92,9 +92,9 @@ class InterrogationForms(db.Model):
"payload": self.payload,
"source_image": self.interrogation.source_image,
}
if self.name in KNOWN_POST_PROCESSORS:
if self.name in IMAGE_RESULT_ALCHEMY_FORMS:
ret_dict["r2_upload"] = generate_procgen_upload_url(str(self.id), False)
logger.debug([self.name in KNOWN_POST_PROCESSORS, self.name, KNOWN_POST_PROCESSORS])
logger.debug([self.name in IMAGE_RESULT_ALCHEMY_FORMS, self.name, IMAGE_RESULT_ALCHEMY_FORMS])
logger.debug(ret_dict)
return ret_dict
@@ -110,7 +110,7 @@ class InterrogationForms(db.Model):
if self.result[form_name] == "R2":
self.result[form_name] = generate_procgen_download_url(str(self.id), False)
if not self.interrogation.r2stored:
if self.name in KNOWN_POST_PROCESSORS:
if self.name in IMAGE_RESULT_ALCHEMY_FORMS:
# Post-processed images live in R2 only for 120 minutes
hr.horde_r_setex(
f"{self.name}_{self.interrogation.source_image}",
@@ -310,7 +310,15 @@ class Interrogation(db.Model):
# Interrogations are more intensive so they reward better
if form["name"] == "interrogation":
kudos = 3
elif form["name"] in KNOWN_POST_PROCESSORS:
elif form["name"] == "annotation":
# The annotation form scales with the source-image size and its detector cost class:
# a control map from a heavy transformers-hub detector earns more per tile than one
# from a weightless OpenCV detector.
control_type = (form.get("payload") or {}).get("control_type")
kudos = self.image_tiles * annotation_detector_kudos_bucket(control_type)
elif form["name"] in IMAGE_RESULT_ALCHEMY_FORMS:
# Post-processors (upscalers, face restorers, strip_background) scale with the
# source-image size, mirroring the upscaler/facefixer pricing.
logger.debug(self.image_tiles)
kudos = self.image_tiles * 3
form_entry = InterrogationForms(
@@ -33,12 +33,25 @@ class WorkerInterrogationForm(db.Model):
form = db.Column(db.String(30))
class WorkerAnnotationType(db.Model):
__tablename__ = "interrogation_worker_annotation_types"
id = db.Column(db.Integer, primary_key=True)
worker_id = db.Column(
uuid_column_type(),
db.ForeignKey("workers.id", ondelete="CASCADE"),
nullable=False,
)
worker = db.relationship("InterrogationWorker", back_populates="annotation_types")
annotation_type = db.Column(db.String(64))
class InterrogationWorker(WorkerTemplate):
__mapper_args__ = {
"polymorphic_identity": "interrogation_worker",
}
forms = db.relationship("WorkerInterrogationForm", back_populates="worker")
annotation_types = db.relationship("WorkerAnnotationType", back_populates="worker")
processing_forms = db.relationship("InterrogationForms", back_populates="worker")
wtype = "interrogation"
@@ -49,6 +62,9 @@ class InterrogationWorker(WorkerTemplate):
# If's OK to provide an empty list here as we don't actually modify this var
# We only check it in can_generate
self.set_forms(kwargs.get("forms"))
# Advertised annotation types are display/moderation only; pop-time matching stays on the
# live pop payload. A legacy alchemist sends no annotation_types, which clears cleanly.
self.set_annotation_types(kwargs.get("annotation_types"))
form_names = self.get_form_names()
if len(form_names) == 0:
self.set_forms(["caption"])
@@ -139,6 +155,30 @@ class InterrogationWorker(WorkerTemplate):
db.session.add(form)
db.session.commit()
def get_annotation_type_names(self):
annotation_type_names = (
db.session.query(func.distinct(WorkerAnnotationType.annotation_type).label("name"))
.filter(WorkerAnnotationType.worker_id == self.id)
.all()
)
return [a.name for a in annotation_type_names]
def set_annotation_types(self, annotation_types):
# A legacy alchemist advertises no annotation types; treat absent as an empty set.
if not annotation_types:
annotation_types = []
# We don't allow workers to claim they can serve more than 100 annotation types (to prevent abuse)
annotation_types = list(annotation_types)[:100]
existing_annotation_types = db.session.query(WorkerAnnotationType).filter_by(worker_id=self.id)
existing_annotation_type_names = set([a.annotation_type for a in existing_annotation_types.all()])
if existing_annotation_type_names == set(annotation_types):
return
existing_annotation_types.delete()
for annotation_type in annotation_types:
entry = WorkerAnnotationType(worker_id=self.id, annotation_type=annotation_type)
db.session.add(entry)
db.session.commit()
def get_performance(self):
performances = [p.performance for p in self.performance]
if len(performances):
@@ -150,4 +190,5 @@ class InterrogationWorker(WorkerTemplate):
def get_details(self, details_privilege=0):
ret_dict = super().get_details(details_privilege)
ret_dict["forms"] = self.get_form_names()
ret_dict["annotation_types"] = self.get_annotation_type_names()
return ret_dict
+48 -1
View File
@@ -8,6 +8,49 @@ import sys
import numpy as np
from loguru import logger
# The kudos model is a frozen artifact whose control_type one-hot slots are the classic types only.
# New unified control types are collapsed onto their closest classic cost-class before the one-hot
# lookup, so the input vector keeps its trained length. This mirrors hordelib's checkpoint-reuse map:
# cheap algorithmic preprocessors land on `canny`, the lineart family lands on `hed`, and `mlsd`
# uses the legacy `hough` slot. Types absent from this map (and already classic) pass through
# unchanged; anything still unknown to KNOWN_CONTROL_TYPES falls back to the neutral "None" slot.
CANONICAL_KUDOS_CONTROL_TYPES = {
"binary": "scribble",
"scribble_xdog": "scribble",
"scribble_pidinet": "scribble",
"pidinet": "hed",
"teed": "hed",
"standard_lineart": "hed",
"lineart": "hed",
"lineart_anime": "hed",
"lineart_anime_denoise": "hed",
"pyracanny": "canny",
"color": "canny",
"shuffle": "canny",
"recolor_luminance": "canny",
"recolor_intensity": "canny",
"tile": "canny",
"tile_ttplanet_guided": "canny",
"tile_ttplanet_simple": "canny",
"midas_depth": "depth",
"zoe_depth": "depth",
"depth_anything": "depth",
"depth_anything_v2": "depth",
"normal_bae": "normal",
"oneformer_ade20k": "seg",
"oneformer_coco": "seg",
"mlsd": "hough",
"openpose": "openpose",
"canny": "canny",
"hed": "hed",
"depth": "depth",
"normal": "normal",
"seg": "seg",
"scribble": "scribble",
"fakescribbles": "fakescribbles",
"hough": "hough",
}
class KudosModel:
"""Calculate kudos for a given horde job payload. Tiny, lightweight cpu model.
@@ -218,7 +261,11 @@ class KudosModel:
data_samplers.append(
payload["sampler_name"] if payload["sampler_name"] in KudosModel.KNOWN_SAMPLERS else "k_euler",
)
data_control_types.append(payload.get("control_type", "None"))
control_type = payload.get("control_type", "None")
mapped_control_type = CANONICAL_KUDOS_CONTROL_TYPES.get(control_type, control_type)
if mapped_control_type not in KudosModel.KNOWN_CONTROL_TYPES:
mapped_control_type = "None"
data_control_types.append(mapped_control_type)
sp = payload.get("source_processing", "txt2img")
# Little hack until new model is out
if sp == "remix":
+9 -1
View File
@@ -11,7 +11,7 @@ from horde.bridge_reference import (
is_official_bridge_version,
)
from horde.classes.base.worker import Worker
from horde.consts import KNOWN_POST_PROCESSORS
from horde.consts import KNOWN_POST_PROCESSORS, LEGACY_IMAGE_CONTROL_TYPES
from horde.flask import db
from horde.logger import logger
from horde.model_reference import model_reference
@@ -28,6 +28,7 @@ class ImageWorker(Worker):
allow_painting = db.Column(db.Boolean, default=True, nullable=False, index=True)
allow_post_processing = db.Column(db.Boolean, default=True, nullable=False, index=True)
allow_controlnet = db.Column(db.Boolean, default=False, nullable=False, index=True)
allow_extended_controlnet = db.Column(db.Boolean, default=False, nullable=False, index=True)
allow_sdxl_controlnet = db.Column(db.Boolean, default=False, nullable=False, index=True)
allow_lora = db.Column(db.Boolean, default=False, nullable=False, index=True)
limit_max_steps = db.Column(db.Boolean, default=False, nullable=False, index=True)
@@ -44,6 +45,7 @@ class ImageWorker(Worker):
self.allow_painting = kwargs.get("allow_painting", True)
self.allow_post_processing = kwargs.get("allow_post_processing", True)
self.allow_controlnet = kwargs.get("allow_controlnet", False)
self.allow_extended_controlnet = kwargs.get("allow_extended_controlnet", False)
self.allow_sdxl_controlnet = kwargs.get("allow_sdxl_controlnet", False)
self.allow_lora = kwargs.get("allow_lora", False)
self.limit_max_steps = kwargs.get("limit_max_steps", False)
@@ -128,6 +130,11 @@ class ImageWorker(Worker):
return [False, "bridge_version"]
if not check_bridge_capability("image_is_control", self.bridge_agent):
return [False, "bridge_version"]
if waiting_prompt.params["control_type"] not in LEGACY_IMAGE_CONTROL_TYPES:
if not check_bridge_capability("extended_controlnet", self.bridge_agent):
return [False, "bridge_version"]
if not self.allow_extended_controlnet:
return [False, "controlnet"]
if not self.allow_controlnet:
return [False, "controlnet"]
if waiting_prompt.params.get("workflow") == "qr_code":
@@ -218,6 +225,7 @@ class ImageWorker(Worker):
ret_dict["painting"] = self.allow_painting if check_bridge_capability("inpainting", self.bridge_agent) else False
ret_dict["post-processing"] = self.allow_post_processing
ret_dict["controlnet"] = self.allow_controlnet
ret_dict["extended_controlnet"] = self.allow_extended_controlnet
ret_dict["sdxl_controlnet"] = self.allow_sdxl_controlnet
ret_dict["lora"] = self.allow_lora
return ret_dict
+122 -1
View File
@@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later
HORDE_VERSION = "5.1.5"
HORDE_VERSION = "5.1.6"
HORDE_API_VERSION = "2.5"
WHITELISTED_SERVICE_IPS = {
@@ -32,6 +32,127 @@ KNOWN_POST_PROCESSORS = {
"RestoreFormer": 1,
}
# Control-map types the parameterized `annotation` alchemy form can produce. This is the closed set
# the image-utilities backend can serve. It spells the line detector `mlsd` (its real name) where
# legacy image-generation control_type still spells the same detector `hough`.
KNOWN_CONTROL_TYPES = [
"canny",
"hed",
"depth",
"mlsd",
"openpose",
"normal",
"scribble",
"fakescribbles",
"seg",
"binary",
"standard_lineart",
"lineart",
"lineart_anime",
"lineart_anime_denoise",
"pidinet",
"scribble_xdog",
"scribble_pidinet",
"teed",
"pyracanny",
"midas_depth",
"zoe_depth",
"depth_anything",
"depth_anything_v2",
"normal_bae",
"oneformer_ade20k",
"oneformer_coco",
"color",
"shuffle",
"recolor_luminance",
"recolor_intensity",
"tile",
"tile_ttplanet_guided",
"tile_ttplanet_simple",
]
# The classic image-generation control types renderable by legacy workers (pre image-utilities).
# Old bridges spell the line detector `hough`; KNOWN_CONTROL_TYPES spells the same detector `mlsd`.
# A control_type outside this set is only dispatchable to bridge agents new enough to annotate it.
LEGACY_IMAGE_CONTROL_TYPES = [
"canny",
"hed",
"depth",
"normal",
"openpose",
"seg",
"scribble",
"fakescribbles",
"hough",
]
# The full set the image-generation `control_type` field accepts: the unified KNOWN_CONTROL_TYPES
# plus the legacy `hough` alias for `mlsd`, kept so existing clients keep validating.
IMAGE_CONTROL_TYPES = [*KNOWN_CONTROL_TYPES, "hough"]
# Alchemy/interrogation forms whose result is an image delivered via R2 (upload URL minted on pop,
# short-lived result cache) rather than an inline JSON payload. Post-processors are image-output by
# nature; the controlnet `annotation` form also emits an image (the control map).
IMAGE_RESULT_ALCHEMY_FORMS = set(KNOWN_POST_PROCESSORS) | {"annotation"}
# Detector cost classes for the parameterized `annotation` alchemy form, sourced from the
# image-utilities annotator registry runtimes. Each control type maps to a per-tile kudos
# multiplier reflecting the horde resources its detector consumes:
# - WEIGHTLESS: pure OpenCV/numpy detectors that load no model weights.
# - WEIGHTED: small/medium annotator checkpoints (measured ~2-8s CPU).
# - HUB: large ViT/transformers-hub detectors (measured ~13-65s CPU).
# An unlisted control type falls back to the middle WEIGHTED class.
ANNOTATION_KUDOS_BUCKET_WEIGHTLESS = 3
ANNOTATION_KUDOS_BUCKET_WEIGHTED = 5
ANNOTATION_KUDOS_BUCKET_HUB = 8
ANNOTATION_KUDOS_DEFAULT_BUCKET = ANNOTATION_KUDOS_BUCKET_WEIGHTED
ANNOTATION_DETECTOR_KUDOS_BUCKETS = {
"canny": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"binary": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"scribble": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"scribble_xdog": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"pyracanny": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"color": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"shuffle": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"recolor_luminance": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"recolor_intensity": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"tile": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"tile_ttplanet_guided": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"tile_ttplanet_simple": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"standard_lineart": ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
"hed": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"fakescribbles": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"mlsd": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"openpose": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"depth": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"seg": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"lineart": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"lineart_anime": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"lineart_anime_denoise": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"pidinet": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"scribble_pidinet": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"teed": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"normal_bae": ANNOTATION_KUDOS_BUCKET_WEIGHTED,
"midas_depth": ANNOTATION_KUDOS_BUCKET_HUB,
"zoe_depth": ANNOTATION_KUDOS_BUCKET_HUB,
"depth_anything": ANNOTATION_KUDOS_BUCKET_HUB,
"depth_anything_v2": ANNOTATION_KUDOS_BUCKET_HUB,
"normal": ANNOTATION_KUDOS_BUCKET_HUB,
"oneformer_ade20k": ANNOTATION_KUDOS_BUCKET_HUB,
"oneformer_coco": ANNOTATION_KUDOS_BUCKET_HUB,
}
def annotation_detector_kudos_bucket(control_type):
"""Return the per-tile kudos multiplier for an annotation control type.
Unknown or missing control types fall back to the middle WEIGHTED bucket so
a novel detector is never priced below its likely cost class.
"""
return ANNOTATION_DETECTOR_KUDOS_BUCKETS.get(control_type, ANNOTATION_KUDOS_DEFAULT_BUCKET)
KNOWN_UPSCALERS = [
"RealESRGAN_x4plus",
"RealESRGAN_x2plus",
+31
View File
@@ -33,6 +33,7 @@ from horde.classes.stable.interrogation_worker import InterrogationWorker
from horde.classes.stable.processing_generation import ImageProcessingGeneration
from horde.classes.stable.waiting_prompt import ImageWaitingPrompt
from horde.classes.stable.worker import ImageWorker
from horde.consts import LEGACY_IMAGE_CONTROL_TYPES
from horde.database.classes import FakeWPRow
from horde.database.kudos_legacy_projection import consume_user_reservation
from horde.database.kudos_reservations import reserve_kudos
@@ -1009,6 +1010,13 @@ def get_sorted_wp_filtered_to_worker(worker, models_list=None, blacklist=None, p
and_(
worker.allow_controlnet == True, # noqa E712
check_bridge_capability("controlnet", worker.bridge_agent),
or_(
ImageWaitingPrompt.params["control_type"].astext.in_(LEGACY_IMAGE_CONTROL_TYPES),
and_(
check_bridge_capability("extended_controlnet", worker.bridge_agent),
worker.allow_extended_controlnet == True, # noqa E712
),
),
),
),
or_(
@@ -1117,6 +1125,7 @@ def count_skipped_image_wp(worker, models_list=None, blacklist=None, priority_us
can_ti = check_bridge_capability("textual_inversion", bridge_agent)
can_pp = check_bridge_capability("post-processing", bridge_agent)
can_controlnet = check_bridge_capability("controlnet", bridge_agent)
can_extended_controlnet = check_bridge_capability("extended_controlnet", bridge_agent)
can_hires = check_bridge_capability("hires_fix", bridge_agent)
can_return_ctrl = check_bridge_capability("return_control_map", bridge_agent)
can_tiling = check_bridge_capability("tiling", bridge_agent)
@@ -1211,6 +1220,22 @@ def count_skipped_image_wp(worker, models_list=None, blacklist=None, priority_us
# controlnet
if worker.allow_controlnet is False or not can_controlnet:
count_exprs["_controlnet_raw"] = count_distinct_wp(ImageWaitingPrompt.params.has_key("control_type"))
elif not can_extended_controlnet:
# A controlnet-capable but pre-extended worker cannot render control types outside the classic set.
count_exprs["_controlnet_extended_bridge_raw"] = count_distinct_wp(
and_(
ImageWaitingPrompt.params.has_key("control_type"),
ImageWaitingPrompt.params["control_type"].astext.notin_(LEGACY_IMAGE_CONTROL_TYPES),
),
)
elif worker.allow_extended_controlnet is False:
# An extended-capable worker that opted out of extended types skips them by worker choice.
count_exprs["_controlnet_extended_choice_raw"] = count_distinct_wp(
and_(
ImageWaitingPrompt.params.has_key("control_type"),
ImageWaitingPrompt.params["control_type"].astext.notin_(LEGACY_IMAGE_CONTROL_TYPES),
),
)
# performance (slow workers)
if worker.speed <= 500000:
@@ -1337,6 +1362,12 @@ def count_skipped_image_wp(worker, models_list=None, blacklist=None, priority_us
ret_dict["controlnet"] = controlnet_count
else:
bridge_version_count += controlnet_count
extended_controlnet_bridge_count = raw.get("_controlnet_extended_bridge_raw", 0) or 0
if extended_controlnet_bridge_count > 0:
bridge_version_count += extended_controlnet_bridge_count
extended_controlnet_choice_count = raw.get("_controlnet_extended_choice_raw", 0) or 0
if extended_controlnet_choice_count > 0:
ret_dict["controlnet"] = ret_dict.get("controlnet", 0) + extended_controlnet_choice_count
# performance
perf_count = (raw.get("_perf_slow", 0) or 0) + (raw.get("_perf_extra_slow", 0) or 0)
+1 -1
View File
@@ -5,7 +5,7 @@
[project]
name = "AI-Horde"
version = "5.1.3"
version = "5.1.6"
description = "A Crowdsourced Generative AI cluster for everyone."
authors = [
{name = "db0", email = "mail@dbzer0.com"},
+29
View File
@@ -0,0 +1,29 @@
-- Run with psql in its default autocommit mode. Do NOT pass --single-transaction: this
-- file contains CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block.
-- Pass -v ON_ERROR_STOP=1 so a failure halts rather than continuing. Every statement is
-- idempotent, so a halted run can be repeated once the cause is addressed; a failed
-- concurrent index build leaves an INVALID index that must be dropped before the rerun.
--
-- Safe to apply ahead of the deploy. An instance that predates this migration neither
-- selects allow_extended_controlnet nor reads the annotation-type table, and the column
-- default covers worker INSERTs that omit it.
SET lock_timeout = '3s';
-- The worker's opt-in to control types outside the classic set. Defaulting false is what
-- makes the offer fail-closed: an existing worker keeps serving legacy control types only
-- until its operator turns the flag on. The pop candidate query and the skipped-request
-- counts both filter on it, matching the other capability columns on workers.
ALTER TABLE workers ADD COLUMN IF NOT EXISTS allow_extended_controlnet BOOLEAN NOT NULL DEFAULT false;
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_workers_allow_extended_controlnet
ON public.workers USING btree (allow_extended_controlnet);
-- The annotation control types an alchemist advertises at check-in, mirroring how
-- interrogation_worker_forms records its offered forms. This backs worker details and
-- moderation; pop-time matching reads the live pop payload instead, so a stale or empty
-- row set never gates dispatch.
CREATE TABLE IF NOT EXISTS interrogation_worker_annotation_types (
id SERIAL PRIMARY KEY,
worker_id UUID NOT NULL REFERENCES workers(id) ON DELETE CASCADE,
annotation_type VARCHAR(64)
);
+3
View File
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: Tazlin <tazlin.on.github@gmail.com>
SPDX-License-Identifier: AGPL-3.0-or-later
+120
View File
@@ -0,0 +1,120 @@
# SPDX-FileCopyrightText: 2026 Tazlin <tazlin@haidra.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Cross-repo wire-contract client, executed under the local horde_sdk venv.
Reads a JSON job description on stdin, performs real HTTP pops against a live
AI-Horde server, and validates the responses with the local horde_sdk response
models. Emits a single-line JSON verdict on stdout. It imports only horde_sdk,
requests, and the standard library: never the AI-Horde ``horde`` package.
"""
from __future__ import annotations
import json
import sys
import requests
from horde_sdk.ai_horde_api.apimodels.alchemy.pop import AlchemyJobPopResponse, AlchemyPopRequest
from horde_sdk.ai_horde_api.apimodels.generate.pop import (
ImageGenerateJobPopRequest,
ImageGenerateJobPopResponse,
)
from horde_sdk.generation_parameters.alchemy.consts import KNOWN_ANNOTATION_CONTROL_TYPES
def _headers(cfg: dict) -> dict:
return {"apikey": cfg["apikey"], "Client-Agent": cfg["client_agent"]}
def _check_alchemy(cfg: dict) -> dict:
body = cfg["alchemy_pop_body"]
request_model = AlchemyPopRequest(
apikey=cfg["apikey"],
name=body["name"],
priority_usernames=[],
forms=body["forms"],
annotation_types=body["annotation_types"],
amount=1,
)
reserialized = request_model.model_dump(by_alias=True, exclude_none=True, mode="json")
if reserialized.get("annotation_types") != body["annotation_types"]:
raise AssertionError(f"SDK AlchemyPopRequest dropped annotation_types: {reserialized!r}")
response = requests.post(cfg["base_url"] + "/api/v2/interrogate/pop", json=body, headers=_headers(cfg), timeout=30)
response.raise_for_status()
raw = response.json()
parsed = AlchemyJobPopResponse.model_validate(raw)
forms = parsed.forms or []
if not forms:
raise AssertionError(f"alchemy pop returned no forms: {raw!r}")
form = forms[0]
if form.form != "annotation":
raise AssertionError(f"unexpected form: {form.form!r} in {raw!r}")
if form.payload is None or form.payload.control_type is None:
raise AssertionError(f"SDK did not parse control_type from payload: {raw!r}")
if form.payload.control_type != KNOWN_ANNOTATION_CONTROL_TYPES.canny:
raise AssertionError(f"control_type did not round-trip: {form.payload.control_type!r}")
if not form.r2_upload:
raise AssertionError(f"annotation pop missing r2_upload: {raw!r}")
return {
"form": str(form.form),
"control_type": str(form.payload.control_type),
"control_type_is_enum": isinstance(form.payload.control_type, KNOWN_ANNOTATION_CONTROL_TYPES),
"has_r2_upload": bool(form.r2_upload),
"request_roundtrip_annotation_types": reserialized.get("annotation_types"),
}
def _check_image(cfg: dict) -> dict:
body = cfg["image_pop_body"]
request_model = ImageGenerateJobPopRequest(
apikey=cfg["apikey"],
name=body["name"],
priority_usernames=[],
models=body["models"],
bridge_agent=body["bridge_agent"],
amount=body["amount"],
max_pixels=body["max_pixels"],
allow_controlnet=True,
allow_extended_controlnet=body["allow_extended_controlnet"],
)
reserialized = request_model.model_dump(by_alias=True, exclude_none=True, mode="json")
if reserialized.get("allow_extended_controlnet") is not True:
raise AssertionError(f"SDK ImageGenerateJobPopRequest dropped allow_extended_controlnet: {reserialized!r}")
response = requests.post(cfg["base_url"] + "/api/v2/generate/pop", json=body, headers=_headers(cfg), timeout=30)
response.raise_for_status()
raw = response.json()
parsed = ImageGenerateJobPopResponse.model_validate(raw)
if parsed.id_ is None:
raise AssertionError(f"image pop matched no job: {raw!r}")
if parsed.payload is None or parsed.payload.control_type is None:
raise AssertionError(f"SDK did not parse control_type from image payload: {raw!r}")
return {
"id_present": parsed.id_ is not None,
"control_type": str(parsed.payload.control_type),
"request_roundtrip_allow_extended_controlnet": reserialized.get("allow_extended_controlnet"),
}
def main() -> int:
cfg = json.load(sys.stdin)
verdict = {
"alchemy": _check_alchemy(cfg),
"image": _check_image(cfg),
"ok": True,
}
sys.stdout.write(json.dumps(verdict))
return 0
if __name__ == "__main__":
sys.exit(main())
+182
View File
@@ -3,6 +3,15 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import pytest
# The annotation form is image-output: its pop mints an R2 upload URL, so the object store must be
# provisioned for this module rather than depending on a co-running marked module to bring it up.
pytestmark = [
pytest.mark.object_storage,
pytest.mark.usefixtures("object_store_ready"),
]
def test_simple_alchemy(client, request_headers: dict[str, str]) -> None:
async_dict = {
@@ -114,6 +123,179 @@ def test_alchemy_vectorize(client, request_headers: dict[str, str]) -> None:
assert retrieve_results["state"] == "done"
def test_alchemy_annotation(client, request_headers: dict[str, str]) -> None:
async_dict = {
"forms": [
{"name": "annotation", "payload": {"control_type": "canny"}},
],
"source_image": "https://github.com/Haidra-Org/AI-Horde/blob/main/icon.png?raw=true",
}
async_req = client.post("/api/v2/interrogate/async", json=async_dict, headers=request_headers)
assert async_req.status_code < 400, async_req.get_data(as_text=True)
async_results = async_req.get_json()
req_id = async_results["id"]
pop_dict = {
"name": "CICD Fake Alchemist",
"forms": ["annotation"],
"annotation_types": ["canny"],
"bridge_agent": request_headers["Client-Agent"],
"max_tiles": 96,
}
try:
pop_req = client.post("/api/v2/interrogate/pop", json=pop_dict, headers=request_headers)
except Exception:
client.delete(f"/api/v2/interrogate/status/{req_id}", headers=request_headers)
raise
assert pop_req.status_code < 400, pop_req.get_data(as_text=True)
pop_results = pop_req.get_json()
popped_form = pop_results["forms"][0]
job_id = popped_form["id"]
assert job_id is not None, pop_results
assert popped_form["form"] == "annotation"
# annotation is an image-output form, so the pop must carry an R2 upload destination.
assert popped_form["r2_upload"], pop_results
# The parameterized control type must survive round-trip to the worker via the form payload.
assert popped_form["payload"]["control_type"] == "canny", pop_results
submit_dict = {
"id": job_id,
"result": {"annotation": "R2"},
"state": "ok",
}
submit_req = client.post("/api/v2/interrogate/submit", json=submit_dict, headers=request_headers)
assert submit_req.status_code < 400, submit_req.get_data(as_text=True)
submit_results = submit_req.get_json()
assert submit_results["reward"] > 0
retrieve_req = client.get(f"/api/v2/interrogate/status/{req_id}", headers=request_headers)
assert retrieve_req.status_code < 400, retrieve_req.get_data(as_text=True)
retrieve_results = retrieve_req.get_json()
assert len(retrieve_results["forms"]) == 1
gen = retrieve_results["forms"][0]
assert gen["form"] == "annotation"
assert gen["state"] == "done"
assert retrieve_results["state"] == "done"
def _annotation_reward_for_control_type(client, request_headers: dict[str, str], control_type: str) -> float:
"""Run a full annotation pop->submit cycle for ``control_type`` and return the awarded kudos."""
async_dict = {
"forms": [
{"name": "annotation", "payload": {"control_type": control_type}},
],
"source_image": "https://github.com/Haidra-Org/AI-Horde/blob/main/icon.png?raw=true",
}
async_req = client.post("/api/v2/interrogate/async", json=async_dict, headers=request_headers)
assert async_req.status_code < 400, async_req.get_data(as_text=True)
req_id = async_req.get_json()["id"]
try:
pop_dict = {
"name": "CICD Fake Alchemist",
"forms": ["annotation"],
"annotation_types": [control_type],
"bridge_agent": request_headers["Client-Agent"],
"max_tiles": 96,
}
pop_req = client.post("/api/v2/interrogate/pop", json=pop_dict, headers=request_headers)
assert pop_req.status_code < 400, pop_req.get_data(as_text=True)
job_id = pop_req.get_json()["forms"][0]["id"]
submit_dict = {"id": job_id, "result": {"annotation": "R2"}, "state": "ok"}
submit_req = client.post("/api/v2/interrogate/submit", json=submit_dict, headers=request_headers)
assert submit_req.status_code < 400, submit_req.get_data(as_text=True)
return submit_req.get_json()["reward"]
finally:
client.delete(f"/api/v2/interrogate/status/{req_id}", headers=request_headers)
def test_alchemy_annotation_bucket_prices_by_detector_class(client, request_headers: dict[str, str]) -> None:
"""Pricing uses the detector cost bucket: a heavy hub detector (oneformer) pays more per tile
than a weightless one (canny) for the same source image."""
canny_reward = _annotation_reward_for_control_type(client, request_headers, "canny")
oneformer_reward = _annotation_reward_for_control_type(client, request_headers, "oneformer_ade20k")
assert canny_reward > 0
assert oneformer_reward > canny_reward, (canny_reward, oneformer_reward)
def _pop_annotation(client, request_headers: dict[str, str], pop_dict: dict) -> dict:
async_dict = {
"forms": [
{"name": "annotation", "payload": {"control_type": "canny"}},
],
"source_image": "https://github.com/Haidra-Org/AI-Horde/blob/main/icon.png?raw=true",
}
async_req = client.post("/api/v2/interrogate/async", json=async_dict, headers=request_headers)
assert async_req.status_code < 400, async_req.get_data(as_text=True)
req_id = async_req.get_json()["id"]
try:
pop_req = client.post("/api/v2/interrogate/pop", json=pop_dict, headers=request_headers)
assert pop_req.status_code < 400, pop_req.get_data(as_text=True)
return pop_req.get_json()
finally:
client.delete(f"/api/v2/interrogate/status/{req_id}", headers=request_headers)
def test_alchemy_annotation_non_matching_type_does_not_pop(client, request_headers: dict[str, str]) -> None:
"""A worker advertising annotation types that exclude the job's control_type receives no job."""
pop_results = _pop_annotation(
client,
request_headers,
{
"name": "CICD Fake Alchemist",
"forms": ["annotation"],
"annotation_types": ["depth"],
"bridge_agent": request_headers["Client-Agent"],
"max_tiles": 96,
},
)
assert not pop_results.get("forms"), pop_results
def test_alchemy_annotation_absent_types_does_not_pop(client, request_headers: dict[str, str]) -> None:
"""The annotation form is fail-closed: a worker that advertises no annotation types matches no jobs."""
pop_results = _pop_annotation(
client,
request_headers,
{
"name": "CICD Fake Alchemist",
"forms": ["annotation"],
"bridge_agent": request_headers["Client-Agent"],
"max_tiles": 96,
},
)
assert not pop_results.get("forms"), pop_results
def test_alchemy_annotation_rejects_unknown_control_type(client, request_headers: dict[str, str]) -> None:
"""The server validates control_type against its closed set and rejects anything outside it."""
async_dict = {
"forms": [
{"name": "annotation", "payload": {"control_type": "not_a_real_detector"}},
],
"source_image": "https://github.com/Haidra-Org/AI-Horde/blob/main/icon.png?raw=true",
}
async_req = client.post("/api/v2/interrogate/async", json=async_dict, headers=request_headers)
assert async_req.status_code == 400, async_req.get_data(as_text=True)
def test_alchemy_annotation_requires_control_type(client, request_headers: dict[str, str]) -> None:
"""An annotation form with no control_type payload is rejected rather than silently defaulted."""
async_dict = {
"forms": [
{"name": "annotation"},
],
"source_image": "https://github.com/Haidra-Org/AI-Horde/blob/main/icon.png?raw=true",
}
async_req = client.post("/api/v2/interrogate/async", json=async_dict, headers=request_headers)
assert async_req.status_code == 400, async_req.get_data(as_text=True)
def test_alchemist_palette_and_describe(client, request_headers: dict[str, str]) -> None:
async_dict = {
"forms": [
@@ -0,0 +1,162 @@
# SPDX-FileCopyrightText: 2026 Tazlin <tazlin@haidra.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Persistence + backwards-compatibility coverage for advertised annotation types.
An alchemy worker's advertised ``annotation_types`` are stored for display and
moderation only. Pop-time matching stays on the live pop payload (fail-closed),
so these tests assert:
- check_in persists the advertised set and updates it on the next check-in;
- worker details expose the set additively;
- a legacy alchemist (no annotation_types anywhere) keeps working unchanged.
"""
from __future__ import annotations
import pytest
AGENT = "aihorde_ci_client:1.0:(test)ci"
@pytest.fixture(autouse=True)
def _no_rate_limit():
from horde.limiter import limiter
previous = limiter.enabled
limiter.enabled = False
yield
limiter.enabled = previous
def _headers(api_key: str) -> dict[str, str]:
return {"apikey": api_key, "Client-Agent": AGENT}
def _expire_caches() -> None:
"""Flush the (fake) redis caches so a details read reflects committed DB state
rather than the snapshot taken at check-in time."""
from horde import horde_redis as horde_redis_module
redis_conn = horde_redis_module.horde_redis
seen: dict[int, object] = {}
for client in [redis_conn.horde_r, redis_conn.horde_local_r, *redis_conn.all_horde_redis]:
if client is not None:
seen[id(client)] = client
for client in seen.values():
try:
client.flushdb()
except Exception:
continue
def _worker_details_by_name(client, api_key: str, worker_name: str) -> dict:
_expire_caches()
resp = client.get(f"/api/v2/workers/name/{worker_name}", headers=_headers(api_key))
assert resp.status_code == 200, resp.get_data(as_text=True)
return resp.get_json()
def _alchemy_pop(client, api_key: str, pop_dict: dict):
return client.post("/api/v2/interrogate/pop", json=pop_dict, headers=_headers(api_key))
class TestAnnotationTypePersistence:
def test_check_in_persists_and_updates_the_advertised_set(self, client, api_key):
worker_name = "AnnotationPersistWorker"
first = _alchemy_pop(
client,
api_key,
{
"name": worker_name,
"forms": ["caption"],
"annotation_types": ["canny", "depth"],
"bridge_agent": AGENT,
"max_tiles": 96,
},
)
assert first.status_code == 200, first.get_data(as_text=True)
details = _worker_details_by_name(client, api_key, worker_name)
assert "annotation_types" in details
assert set(details["annotation_types"]) == {"canny", "depth"}
# Additive only: the legacy forms key still exists and keeps its shape.
assert set(details["forms"]) == {"caption"}
# A subsequent check-in with a different set replaces (not accumulates) it.
second = _alchemy_pop(
client,
api_key,
{
"name": worker_name,
"forms": ["caption"],
"annotation_types": ["seg"],
"bridge_agent": AGENT,
"max_tiles": 96,
},
)
assert second.status_code == 200, second.get_data(as_text=True)
details = _worker_details_by_name(client, api_key, worker_name)
assert set(details["annotation_types"]) == {"seg"}
def test_absurdly_long_advertised_list_is_capped(self, app, client, api_key):
# The pop endpoint already validates advertised types against the known set, so the
# internal 100-entry abuse cap is exercised directly on the model here.
worker_name = "AnnotationCapWorker"
resp = _alchemy_pop(
client,
api_key,
{"name": worker_name, "forms": ["caption"], "bridge_agent": AGENT, "max_tiles": 96},
)
assert resp.status_code == 200, resp.get_data(as_text=True)
from horde.classes.stable.interrogation_worker import InterrogationWorker
from horde.flask import db
with app.app_context():
worker = db.session.query(InterrogationWorker).filter_by(name=worker_name).first()
assert worker is not None
worker.set_annotation_types([f"type_{i}" for i in range(250)])
names = worker.get_annotation_type_names()
assert len(names) == 100
class TestLegacyAlchemistBackwardsCompatibility:
def test_legacy_alchemist_pops_a_queued_form_and_raises_nothing(self, client, api_key):
"""A legacy alchemist (no annotation_types anywhere, only legacy forms) still receives a
queued legacy form and its check-in/details flow raises nothing."""
async_dict = {
"forms": [
{"name": "caption"},
],
"source_image": "https://github.com/Haidra-Org/AI-Horde/blob/main/icon.png?raw=true",
}
async_req = client.post("/api/v2/interrogate/async", json=async_dict, headers=_headers(api_key))
assert async_req.status_code < 400, async_req.get_data(as_text=True)
req_id = async_req.get_json()["id"]
worker_name = "LegacyAlchemist"
try:
pop_dict = {
"name": worker_name,
"forms": ["caption", "strip_background"],
"bridge_agent": AGENT,
"max_tiles": 96,
}
pop_req = _alchemy_pop(client, api_key, pop_dict)
assert pop_req.status_code == 200, pop_req.get_data(as_text=True)
pop_results = pop_req.get_json()
# The legacy worker still gets the queued caption job with no annotation gating.
assert pop_results.get("forms"), pop_results
assert pop_results["forms"][0]["form"] == "caption", pop_results
# Details flow raises nothing and reports an empty (but present) annotation set.
details = _worker_details_by_name(client, api_key, worker_name)
assert details["annotation_types"] == []
assert "caption" in details["forms"]
finally:
client.delete(f"/api/v2/interrogate/status/{req_id}", headers=_headers(api_key))
@@ -0,0 +1,174 @@
# SPDX-FileCopyrightText: 2026 Tazlin <tazlin@haidra.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Dispatch gating for the unified image-generation control types.
The classic controlnet set is renderable by any controlnet-capable worker. The
extended set (everything outside ``LEGACY_IMAGE_CONTROL_TYPES``) may only be
dispatched to reGen bridge agents new enough to annotate it, otherwise the job
would be sent to a worker that cannot fulfil it. The legacy ``hough`` alias must
keep matching old workers unchanged.
"""
import base64
from io import BytesIO
import pytest
from PIL import Image
from horde.bridge_reference import EXTENDED_CONTROLNET_REGEN_VERSION
TEST_MODELS = ["stable_diffusion"]
SDXL_MODELS = ["AlbedoBase XL (SDXL)"]
# A pre-extended reGen agent: has classic controlnet, lacks extended controlnet.
OLD_BRIDGE_AGENT = "AI Horde Worker reGen:13:https://github.com/Haidra-Org/horde-worker-reGen"
# A reGen agent at the extended controlnet threshold.
NEW_BRIDGE_AGENT = f"AI Horde Worker reGen:{EXTENDED_CONTROLNET_REGEN_VERSION}:https://github.com/Haidra-Org/horde-worker-reGen"
pytestmark = [
pytest.mark.object_storage,
pytest.mark.usefixtures("object_store_ready"),
]
def _load_image_as_b64(image_path: str) -> str:
final_src_img = Image.open(image_path)
buffer = BytesIO()
final_src_img.save(buffer, format="Webp", quality=50, exact=True)
return base64.b64encode(buffer.getvalue()).decode("utf8")
def _controlnet_async_dict(control_type: str, models: list[str] | None = None) -> dict:
return {
"prompt": "a controlnet-guided horde of robots",
"nsfw": True,
"censor_nsfw": False,
"r2": True,
"shared": True,
"trusted_workers": True,
"params": {
"width": 512,
"height": 512,
"steps": 20,
"cfg_scale": 7.5,
"sampler_name": "k_euler_a",
"control_type": control_type,
},
"models": models if models is not None else TEST_MODELS,
"source_image": _load_image_as_b64("img_stable/0.jpg"),
"source_processing": "img2img",
}
def _pop_dict(bridge_agent: str, allow_extended_controlnet: bool = True) -> dict:
return {
"name": "CICD Fake Dreamer",
"models": TEST_MODELS,
"bridge_agent": bridge_agent,
"nsfw": True,
"amount": 10,
"max_pixels": 4194304,
"allow_img2img": True,
"allow_painting": True,
"allow_unsafe_ipaddr": True,
"allow_post_processing": True,
"allow_controlnet": True,
"allow_extended_controlnet": allow_extended_controlnet,
"allow_sdxl_controlnet": True,
"allow_lora": True,
}
def test_new_control_type_skips_old_bridge_but_matches_new(client, request_headers: dict[str, str]) -> None:
async_req = client.post("/api/v2/generate/async", json=_controlnet_async_dict("lineart"), headers=request_headers)
# The unified control set is accepted by the async validator.
assert async_req.status_code < 400, async_req.get_data(as_text=True)
req_id = async_req.get_json()["id"]
try:
old_pop = client.post("/api/v2/generate/pop", json=_pop_dict(OLD_BRIDGE_AGENT), headers=request_headers)
assert old_pop.status_code < 400, old_pop.get_data(as_text=True)
old_results = old_pop.get_json()
# A pre-extended controlnet worker cannot annotate `lineart`, so it is skipped as too old.
assert old_results["id"] is None, old_results
assert old_results["skipped"].get("bridge_version", 0) >= 1, old_results
new_pop = client.post("/api/v2/generate/pop", json=_pop_dict(NEW_BRIDGE_AGENT), headers=request_headers)
assert new_pop.status_code < 400, new_pop.get_data(as_text=True)
new_results = new_pop.get_json()
# The extended-capable worker with the opt-in flag matches the same job.
assert new_results["id"] is not None, new_results
assert new_results["payload"]["control_type"] == "lineart", new_results
finally:
client.delete(f"/api/v2/generate/status/{req_id}", headers=request_headers)
def test_new_control_type_skips_new_bridge_when_flag_off(client, request_headers: dict[str, str]) -> None:
async_req = client.post("/api/v2/generate/async", json=_controlnet_async_dict("lineart"), headers=request_headers)
assert async_req.status_code < 400, async_req.get_data(as_text=True)
req_id = async_req.get_json()["id"]
try:
pop = client.post(
"/api/v2/generate/pop",
json=_pop_dict(NEW_BRIDGE_AGENT, allow_extended_controlnet=False),
headers=request_headers,
)
assert pop.status_code < 400, pop.get_data(as_text=True)
results = pop.get_json()
# An extended-capable bridge that opts out of extended types must not receive `lineart`.
assert results["id"] is None, results
# The opt-out is a worker-choice skip, attributed to the controlnet bucket rather than bridge_version.
assert results["skipped"].get("controlnet", 0) >= 1, results
finally:
client.delete(f"/api/v2/generate/status/{req_id}", headers=request_headers)
def test_classic_control_type_matches_new_bridge_regardless_of_flag(client, request_headers: dict[str, str]) -> None:
async_req = client.post("/api/v2/generate/async", json=_controlnet_async_dict("canny"), headers=request_headers)
assert async_req.status_code < 400, async_req.get_data(as_text=True)
req_id = async_req.get_json()["id"]
try:
# The extended opt-out must not affect classic control types.
pop = client.post(
"/api/v2/generate/pop",
json=_pop_dict(NEW_BRIDGE_AGENT, allow_extended_controlnet=False),
headers=request_headers,
)
assert pop.status_code < 400, pop.get_data(as_text=True)
results = pop.get_json()
assert results["id"] is not None, results
assert results["payload"]["control_type"] == "canny", results
finally:
client.delete(f"/api/v2/generate/status/{req_id}", headers=request_headers)
@pytest.mark.parametrize("control_type", ["canny", "teed"])
def test_sdxl_baseline_rejects_controlnet(client, request_headers: dict[str, str], control_type: str) -> None:
async_req = client.post(
"/api/v2/generate/async",
json=_controlnet_async_dict(control_type, models=SDXL_MODELS),
headers=request_headers,
)
# An SDXL-baseline model rejects any control_type at validation, classic or extended alike.
assert async_req.status_code == 400, async_req.get_data(as_text=True)
assert async_req.get_json().get("rc") == "ControlNetMismatch", async_req.get_data(as_text=True)
def test_legacy_hough_alias_still_matches_old_bridge(client, request_headers: dict[str, str]) -> None:
async_req = client.post("/api/v2/generate/async", json=_controlnet_async_dict("hough"), headers=request_headers)
assert async_req.status_code < 400, async_req.get_data(as_text=True)
req_id = async_req.get_json()["id"]
try:
old_pop = client.post("/api/v2/generate/pop", json=_pop_dict(OLD_BRIDGE_AGENT), headers=request_headers)
assert old_pop.status_code < 400, old_pop.get_data(as_text=True)
old_results = old_pop.get_json()
# `hough` is a classic control type, so an old controlnet worker still fulfils it.
assert old_results["id"] is not None, old_results
assert old_results["payload"]["control_type"] == "hough", old_results
finally:
client.delete(f"/api/v2/generate/status/{req_id}", headers=request_headers)
+162
View File
@@ -0,0 +1,162 @@
# SPDX-FileCopyrightText: 2026 Tazlin <tazlin@haidra.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Cross-repo wire-contract proof: live HTTP server parsed by the local horde_sdk.
The unit/integration suites exercise the server in isolation. This module proves
the wire contract between the AI-Horde server tree and the horde_sdk tree: the
server's real HTTP pop responses parse into the local horde_sdk response models
with the new annotation/extended-controlnet fields intact, and the SDK's request
models re-serialize those same fields. The SDK runs in its own venv as a
subprocess so the two trees never share an interpreter.
"""
from __future__ import annotations
import base64
import json
import os
import socket
import subprocess
import threading
from collections.abc import Iterator
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image
from werkzeug.serving import make_server
from horde.bridge_reference import EXTENDED_CONTROLNET_REGEN_VERSION
# The SDK-side interpreter is host-specific: point HORDE_SDK_WIRE_PYTHON at a python whose
# environment has the horde_sdk under test installed; the suite skips when it is not set.
SDK_PYTHON = Path(os.environ["HORDE_SDK_WIRE_PYTHON"]) if os.environ.get("HORDE_SDK_WIRE_PYTHON") else None
SDK_CLIENT_SCRIPT = Path(__file__).resolve().parent / "_sdk_wire_client.py"
TEST_MODELS = ["stable_diffusion"]
NEW_BRIDGE_AGENT = f"AI Horde Worker reGen:{EXTENDED_CONTROLNET_REGEN_VERSION}:https://github.com/Haidra-Org/horde-worker-reGen"
pytestmark = [
pytest.mark.object_storage,
pytest.mark.usefixtures("object_store_ready"),
pytest.mark.skipif(
SDK_PYTHON is None or not SDK_PYTHON.exists(),
reason="HORDE_SDK_WIRE_PYTHON not set or does not exist",
),
]
def _load_image_as_b64(image_path: str) -> str:
final_src_img = Image.open(image_path)
buffer = BytesIO()
final_src_img.save(buffer, format="Webp", quality=50, exact=True)
return base64.b64encode(buffer.getvalue()).decode("utf8")
@pytest.fixture
def live_server(app) -> Iterator[str]:
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
probe.close()
server = make_server("127.0.0.1", port, app, threaded=True)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{port}"
finally:
server.shutdown()
thread.join(timeout=5)
def test_sdk_parses_live_pop_responses(
client,
live_server: str,
api_key: str,
request_headers: dict[str, str],
) -> None:
annotation_async = {
"forms": [{"name": "annotation", "payload": {"control_type": "canny"}}],
"source_image": "https://github.com/Haidra-Org/AI-Horde/blob/main/icon.png?raw=true",
}
annotation_req = client.post("/api/v2/interrogate/async", json=annotation_async, headers=request_headers)
assert annotation_req.status_code < 400, annotation_req.get_data(as_text=True)
annotation_id = annotation_req.get_json()["id"]
image_async = {
"prompt": "a controlnet-guided horde of robots",
"nsfw": True,
"censor_nsfw": False,
"r2": True,
"shared": True,
"trusted_workers": True,
"params": {
"width": 512,
"height": 512,
"steps": 20,
"cfg_scale": 7.5,
"sampler_name": "k_euler_a",
"control_type": "lineart",
},
"models": TEST_MODELS,
"source_image": _load_image_as_b64("img_stable/0.jpg"),
"source_processing": "img2img",
}
image_req = client.post("/api/v2/generate/async", json=image_async, headers=request_headers)
assert image_req.status_code < 400, image_req.get_data(as_text=True)
image_id = image_req.get_json()["id"]
cfg = {
"base_url": live_server,
"apikey": api_key,
"client_agent": request_headers["Client-Agent"],
"alchemy_pop_body": {
"name": "CICD Fake Alchemist",
"forms": ["annotation"],
"annotation_types": ["canny"],
"bridge_agent": request_headers["Client-Agent"],
"max_tiles": 96,
},
"image_pop_body": {
"name": "CICD Fake Dreamer",
"models": TEST_MODELS,
"bridge_agent": NEW_BRIDGE_AGENT,
"nsfw": True,
"amount": 10,
"max_pixels": 4194304,
"allow_img2img": True,
"allow_painting": True,
"allow_unsafe_ipaddr": True,
"allow_post_processing": True,
"allow_controlnet": True,
"allow_extended_controlnet": True,
"allow_sdxl_controlnet": True,
"allow_lora": True,
},
}
try:
completed = subprocess.run(
[str(SDK_PYTHON), str(SDK_CLIENT_SCRIPT)],
input=json.dumps(cfg),
capture_output=True,
text=True,
cwd=str(SDK_CLIENT_SCRIPT.parent),
timeout=120,
)
assert completed.returncode == 0, (
f"SDK wire client failed (rc={completed.returncode}).\nSTDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}"
)
verdict = json.loads(completed.stdout.strip().splitlines()[-1])
assert verdict["ok"] is True, verdict
assert verdict["alchemy"]["control_type"] == "canny", verdict
assert verdict["alchemy"]["has_r2_upload"] is True, verdict
assert verdict["alchemy"]["request_roundtrip_annotation_types"] == ["canny"], verdict
assert verdict["image"]["control_type"] == "lineart", verdict
assert verdict["image"]["request_roundtrip_allow_extended_controlnet"] is True, verdict
finally:
client.delete(f"/api/v2/interrogate/status/{annotation_id}", headers=request_headers)
client.delete(f"/api/v2/generate/status/{image_id}", headers=request_headers)
+45
View File
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 Tazlin <tazlin.on.github@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Capability gating in ``horde.bridge_reference.check_bridge_capability``.
The extended controlnet gate keeps image-generation jobs whose ``control_type``
falls outside the classic set from being dispatched to bridge agents too old to
annotate them. A regression here would either strand the new control types (no
worker ever matches) or silently route them to workers that cannot render them.
"""
from __future__ import annotations
import pytest
from horde.bridge_reference import EXTENDED_CONTROLNET_REGEN_VERSION, check_bridge_capability
pytestmark = pytest.mark.unit
def _regen_agent(version) -> str:
return f"AI Horde Worker reGen:{version}:https://github.com/Haidra-Org/horde-worker-reGen"
class TestExtendedControlnetCapability:
def test_classic_controlnet_available_on_old_regen(self):
# The classic controlnet capability predates the extended gate.
assert check_bridge_capability("controlnet", _regen_agent(13)) is True
def test_extended_absent_below_threshold(self):
agent = _regen_agent(EXTENDED_CONTROLNET_REGEN_VERSION - 1)
assert check_bridge_capability("extended_controlnet", agent) is False
def test_extended_present_at_threshold(self):
agent = _regen_agent(EXTENDED_CONTROLNET_REGEN_VERSION)
assert check_bridge_capability("extended_controlnet", agent) is True
def test_extended_present_above_threshold(self):
agent = _regen_agent(EXTENDED_CONTROLNET_REGEN_VERSION + 5)
assert check_bridge_capability("extended_controlnet", agent) is True
def test_legacy_cpp_worker_never_gets_extended(self):
# The legacy "AI Horde Worker" agent has no extended controlnet path at any version.
assert check_bridge_capability("extended_controlnet", "AI Horde Worker:99:https://x") is False
+97
View File
@@ -3,10 +3,19 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from horde.consts import (
ANNOTATION_DETECTOR_KUDOS_BUCKETS,
ANNOTATION_KUDOS_BUCKET_HUB,
ANNOTATION_KUDOS_BUCKET_WEIGHTED,
ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
ANNOTATION_KUDOS_DEFAULT_BUCKET,
IMAGE_CONTROL_TYPES,
KNOWN_CONTROL_TYPES,
KNOWN_POST_PROCESSORS,
KNOWN_SAMPLERS,
KNOWN_UPSCALERS,
LEGACY_IMAGE_CONTROL_TYPES,
SECOND_ORDER_SAMPLERS,
annotation_detector_kudos_bucket,
)
@@ -32,3 +41,91 @@ class TestKnownUpscalers:
def test_upscalers_are_post_processors(self):
for upscaler in KNOWN_UPSCALERS:
assert upscaler in KNOWN_POST_PROCESSORS, f"Upscaler '{upscaler}' not in KNOWN_POST_PROCESSORS"
class TestImageControlTypes:
def test_image_control_types_derive_from_unified_list(self):
# The image-generation enum is the unified control set plus the legacy `hough` alias.
assert IMAGE_CONTROL_TYPES == [*KNOWN_CONTROL_TYPES, "hough"]
def test_legacy_alias_hough_is_accepted(self):
# Existing clients still send `hough` for the detector the unified list spells `mlsd`.
assert "hough" in IMAGE_CONTROL_TYPES
assert "mlsd" in IMAGE_CONTROL_TYPES
def test_new_types_are_accepted_by_image_enum(self):
for new_type in ("lineart", "teed", "depth_anything_v2"):
assert new_type in IMAGE_CONTROL_TYPES
def test_legacy_set_is_the_classic_nine(self):
assert LEGACY_IMAGE_CONTROL_TYPES == [
"canny",
"hed",
"depth",
"normal",
"openpose",
"seg",
"scribble",
"fakescribbles",
"hough",
]
def test_legacy_types_are_all_dispatchable(self):
# Every legacy type except the `hough` alias is present in the unified list verbatim.
for legacy_type in LEGACY_IMAGE_CONTROL_TYPES:
assert legacy_type in IMAGE_CONTROL_TYPES
def test_mlsd_is_an_extended_type(self):
# `mlsd` uses a spelling old workers do not understand, so it gates as a new type.
assert "mlsd" not in LEGACY_IMAGE_CONTROL_TYPES
class TestAnnotationKudosBuckets:
_VALID_BUCKETS = {
ANNOTATION_KUDOS_BUCKET_WEIGHTLESS,
ANNOTATION_KUDOS_BUCKET_WEIGHTED,
ANNOTATION_KUDOS_BUCKET_HUB,
}
def test_every_known_control_type_has_an_explicit_bucket(self):
# Lockstep guard: a new control type added to KNOWN_CONTROL_TYPES must be assigned a
# detector cost class explicitly. Silent defaulting is only for unlisted/novel types.
missing = [ct for ct in KNOWN_CONTROL_TYPES if ct not in ANNOTATION_DETECTOR_KUDOS_BUCKETS]
assert not missing, f"KNOWN_CONTROL_TYPES with no explicit kudos bucket: {missing}"
def test_bucket_map_has_no_unknown_control_types(self):
# The mapping should not price detectors the server does not actually advertise.
extra = [ct for ct in ANNOTATION_DETECTOR_KUDOS_BUCKETS if ct not in KNOWN_CONTROL_TYPES]
assert not extra, f"Bucket map references non-KNOWN_CONTROL_TYPES: {extra}"
def test_all_buckets_are_one_of_the_three_classes(self):
for control_type, bucket in ANNOTATION_DETECTOR_KUDOS_BUCKETS.items():
assert bucket in self._VALID_BUCKETS, f"{control_type} has out-of-class bucket {bucket}"
def test_three_classes_are_strictly_ordered(self):
assert ANNOTATION_KUDOS_BUCKET_WEIGHTLESS < ANNOTATION_KUDOS_BUCKET_WEIGHTED < ANNOTATION_KUDOS_BUCKET_HUB
def test_unknown_type_falls_back_to_middle_bucket(self):
assert annotation_detector_kudos_bucket("not_a_real_detector_xyz") == ANNOTATION_KUDOS_DEFAULT_BUCKET
assert ANNOTATION_KUDOS_DEFAULT_BUCKET == ANNOTATION_KUDOS_BUCKET_WEIGHTED
def test_missing_type_falls_back_to_middle_bucket(self):
# A None control_type (no payload) must not raise and lands on the default.
assert annotation_detector_kudos_bucket(None) == ANNOTATION_KUDOS_DEFAULT_BUCKET
def test_helper_agrees_with_mapping_for_known_types(self):
for control_type, bucket in ANNOTATION_DETECTOR_KUDOS_BUCKETS.items():
assert annotation_detector_kudos_bucket(control_type) == bucket
def test_hub_detector_prices_above_weightless_for_same_tiles(self):
# Pricing uses the bucket: an oneformer (HUB) job pays more than a canny (WEIGHTLESS) job
# for the same number of image tiles.
image_tiles = 4
canny_kudos = image_tiles * annotation_detector_kudos_bucket("canny")
oneformer_kudos = image_tiles * annotation_detector_kudos_bucket("oneformer_ade20k")
assert oneformer_kudos > canny_kudos
def test_representative_class_members(self):
assert annotation_detector_kudos_bucket("canny") == ANNOTATION_KUDOS_BUCKET_WEIGHTLESS
assert annotation_detector_kudos_bucket("openpose") == ANNOTATION_KUDOS_BUCKET_WEIGHTED
assert annotation_detector_kudos_bucket("depth_anything_v2") == ANNOTATION_KUDOS_BUCKET_HUB
+65
View File
@@ -211,3 +211,68 @@ class TestUnknownInputsHandled:
img2img = dict(basis_payload, source_processing="img2img", source_image=True)
# Same arithmetic path → same kudos.
assert kudos_model.calculate_kudos(remix) == kudos_model.calculate_kudos(img2img)
class TestControlTypeCanonicalMapping:
"""The kudos checkpoint only has one-hot slots for the classic control types.
Unified control types are collapsed onto their closest classic slot before
the lookup so the trained input vector keeps its length. These tests lock the
vector-length invariant and the specific slot each new type lands on.
Comparisons use ``(a == b).all()`` rather than a framework-specific equality
so they hold whether the feature vector is a torch tensor or a numpy array,
and ``payload_to_tensor`` is the compatibility alias both backends keep.
"""
def test_new_type_preserves_vector_length(self, kudos_model, basis_payload):
from horde.classes.stable.kudos import KudosModel
classic = KudosModel.payload_to_tensor(dict(basis_payload, control_type="canny"))
new_type = KudosModel.payload_to_tensor(dict(basis_payload, control_type="lineart"))
# A changed width here means a new slot leaked into the one-hot, which the
# frozen checkpoint cannot consume.
assert new_type.shape == classic.shape
def test_lineart_family_lands_on_hed_slot(self, kudos_model, basis_payload):
from horde.classes.stable.kudos import KudosModel
for lineart_type in ("lineart", "teed", "pidinet", "standard_lineart"):
mapped = KudosModel.payload_to_tensor(dict(basis_payload, control_type=lineart_type))
hed = KudosModel.payload_to_tensor(dict(basis_payload, control_type="hed"))
assert bool((mapped == hed).all()), lineart_type
def test_cheap_preprocessors_land_on_canny_slot(self, kudos_model, basis_payload):
from horde.classes.stable.kudos import KudosModel
for canny_type in ("pyracanny", "tile", "recolor_luminance", "shuffle"):
mapped = KudosModel.payload_to_tensor(dict(basis_payload, control_type=canny_type))
canny = KudosModel.payload_to_tensor(dict(basis_payload, control_type="canny"))
assert bool((mapped == canny).all()), canny_type
def test_depth_family_lands_on_depth_slot(self, kudos_model, basis_payload):
from horde.classes.stable.kudos import KudosModel
for depth_type in ("midas_depth", "zoe_depth", "depth_anything_v2"):
mapped = KudosModel.payload_to_tensor(dict(basis_payload, control_type=depth_type))
depth = KudosModel.payload_to_tensor(dict(basis_payload, control_type="depth"))
assert bool((mapped == depth).all()), depth_type
def test_mlsd_maps_to_legacy_hough_slot(self, kudos_model, basis_payload):
from horde.classes.stable.kudos import KudosModel
mlsd = KudosModel.payload_to_tensor(dict(basis_payload, control_type="mlsd"))
hough = KudosModel.payload_to_tensor(dict(basis_payload, control_type="hough"))
assert bool((mlsd == hough).all())
def test_unknown_type_falls_back_to_none_slot(self, kudos_model, basis_payload):
from horde.classes.stable.kudos import KudosModel
# An unmapped, unknown type must not raise on the one-hot index lookup.
unknown = KudosModel.payload_to_tensor(dict(basis_payload, control_type="not_a_real_detector_xyz"))
neutral = KudosModel.payload_to_tensor(dict(basis_payload, control_type="None"))
assert bool((unknown == neutral).all())
def test_new_type_prices_without_crashing(self, kudos_model, basis_payload):
priced = kudos_model.calculate_kudos(dict(basis_payload, control_type="teed"))
assert priced > 0