diff --git a/agents/completed.md b/agents/completed.md index 74f07ea..42664b3 100644 --- a/agents/completed.md +++ b/agents/completed.md @@ -8,6 +8,18 @@ --- +# #378: discovery drops variants= from the manifest; model= shorthand breaks with ServerHandle setup param + +**Completed:** yes +**Status:** DONE (2026-07-04, Claude) — two @endpoint API bugs found by the inference-endpoints #343 port. (1) `discover_functions` deduped manifest entries by (declared_module, class_name, python_name) — variants= rows share all three with their base function, so every variant was silently dropped from endpoint.lock; the stamped `name` is now part of the dedup key. (2) The `model=` shorthand counted a `ServerHandle`-annotated setup() param as a slot candidate, so the documented runtime= shape `setup(self, model: str, server: ServerHandle)` failed at decoration; ServerHandle params are now excluded from slot resolution. Regression tests for both; suite 223 passed, 1 skipped. + +## Tasks +- [x] Include entry name in the discovery dedup key (discovery/discover.py). +- [x] Skip ServerHandle-annotated setup params in `_resolve_single_slot` (api/decorators.py). +- [x] Regression tests (manifest keeps variant entries; documented runtime= shape decorates). + +--- + # #377: HF binding files= not honored on every download path (14GB instead of ~3GB) **Completed:** yes diff --git a/agents/progress.md b/agents/progress.md index 9071c82..56bb4ef 100644 --- a/agents/progress.md +++ b/agents/progress.md @@ -7,7 +7,7 @@ > Source for all findings: /home/fidika/cozy/python-gen-worker/AUDIT.md (2026-07-03 full-stack audit, > file:line evidence for every claim). Counterpart protocol/orchestrator issues: tensorhub #503-#510. -next_id: 378 +next_id: 379 --- diff --git a/src/gen_worker/api/decorators.py b/src/gen_worker/api/decorators.py index e5734e0..045f3e2 100644 --- a/src/gen_worker/api/decorators.py +++ b/src/gen_worker/api/decorators.py @@ -255,6 +255,19 @@ def _setup_params(cls: type) -> Optional[dict[str, inspect.Parameter]]: return {p.name: p for p in params} +def _is_server_handle_param(p: inspect.Parameter) -> bool: + """True when the param is annotated ``ServerHandle`` (a runtime= injection + target, not a model slot).""" + ann = p.annotation + if ann is inspect.Parameter.empty: + return False + from ..runtimes.server import ServerHandle + + if ann is ServerHandle: + return True + return isinstance(ann, str) and ann.split(".")[-1] == "ServerHandle" + + def _resolve_single_slot( cls: type, models: dict[str, Binding], @@ -269,6 +282,7 @@ def _resolve_single_slot( named = [ n for n, p in setup_kwargs.items() if p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY) + and not _is_server_handle_param(p) ] if len(named) == 1: models[named[0]] = binding diff --git a/src/gen_worker/discovery/discover.py b/src/gen_worker/discovery/discover.py index 1a64f48..9ded91e 100644 --- a/src/gen_worker/discovery/discover.py +++ b/src/gen_worker/discovery/discover.py @@ -390,13 +390,16 @@ def discover_functions(root: Optional[Path] = None, *, main_module: str | None = ) from e functions: List[Dict[str, Any]] = [] - seen: Set[Tuple[str, str, str]] = set() + seen: Set[Tuple[str, str, str, str]] = set() for f in found: for entry in _extract_entries(f.obj, f.walked_module): + # name is part of the key: variants= share (module, class, + # python_name) with their base function but stamp distinct names. key = ( entry.get("declared_module", entry.get("module", "")), entry.get("class_name", ""), entry.get("python_name", ""), + entry.get("name", ""), ) if key in seen: continue diff --git a/tests/test_discovery_and_decorators.py b/tests/test_discovery_and_decorators.py index 6cc4512..46426df 100644 --- a/tests/test_discovery_and_decorators.py +++ b/tests/test_discovery_and_decorators.py @@ -422,3 +422,56 @@ def test_from_scratch_example_uses_publish_contract() -> None: assert s.output_mode == "single" assert not s.is_async_gen assert s.output_type is mod.FromScratchResult + + +def test_manifest_keeps_variant_entries(tmp_pkg: Path) -> None: + """variants= rows share (module, class, python_name) with their base + function; the manifest dedup must key on the stamped name too.""" + from gen_worker.discovery.discover import discover_functions + + pkg = tmp_pkg / "ep_variants" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "main.py").write_text(textwrap.dedent(""" + import msgspec + from gen_worker import HF, RequestContext, Resources, endpoint + + class In_(msgspec.Struct): + x: str = "" + + class Out_(msgspec.Struct): + y: str + + @endpoint( + model=HF("o/base", dtype="bf16"), + resources=Resources(vram_gb=24), + variants={"generate_fp8": (HF("o/base-fp8"), Resources(vram_gb=14))}, + ) + class Gen: + def setup(self, model: str) -> None: + self.model = model + + def generate(self, ctx: RequestContext, data: In_) -> Out_: + return Out_(y="ok") + """)) + + fns = discover_functions(tmp_pkg, main_module="ep_variants.main") + assert sorted(f["name"] for f in fns) == ["generate", "generate-fp8"] + + +def test_model_shorthand_skips_server_handle_setup_param() -> None: + """runtime= endpoints inject a ServerHandle into setup(); the model= + shorthand must resolve the slot from the remaining parameter.""" + from gen_worker.runtimes.server import ServerHandle + + @endpoint(model=HF("o/llm"), resources=Resources(vram_gb=40), runtime="vllm") + class Chat: + def setup(self, model: str, server: ServerHandle) -> None: + self.base_url = server.base_url + + def complete(self, ctx: RequestContext, data: _In) -> _Out: + return _Out(result="") + + (s,) = extract_specs(Chat) + assert list(s.models) == ["model"] + assert s.models["model"].ref == "o/llm"