Skip to content

Commit a4169e4

Browse files
committed
seeds: pin maxItems to job.count for symmetric bounds
Address Yeming's review feedback on #40. The minItems pin alone left the schema asymmetric (e.g. minItems=27, maxItems=2000), which technically allows the model to over-generate. Over-generation isn't the failure mode that motivated the PR, but pinning both bounds keeps the schema's stated shape aligned with the prompt's stated count. - seeds_response_schema now accepts an optional max_items override; the default behavior (maxItems=2000) is preserved when callers don't pass one, matching the existing seed-validation safety net. - _generate_records now pins both bounds to job.count so each batch produces exactly the requested number of seeds. - Two new unit tests cover the pinned-bounds case and the non-positive override fallback. 30/30 seed-schema tests pass.
1 parent 9aa9ef0 commit a4169e4

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

p2m/stages/seeds.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,18 +159,25 @@ def _validate_tool_source(tool_source: str, target: TargetConfig | None) -> None
159159
def seeds_response_schema(
160160
tool_source: str = TOOL_SOURCE_RUNTIME,
161161
min_items: int | None = None,
162+
max_items: int | None = None,
162163
) -> dict[str, Any]:
163164
"""Full response schema wrapping seeds in a ``{"seeds": [...]}`` envelope.
164165
165166
When ``min_items`` is provided, the schema requires the model to return at
166167
least that many seeds. Without this lower bound, gpt-5.4-mini frequently
167168
returns N-1 items for batches larger than ~10, since the schema previously
168169
allowed 0-2000 items regardless of the prompt's stated count.
170+
171+
When ``max_items`` is provided, it overrides the default 2000 ceiling so
172+
callers can pin both bounds to the exact target count. Pinning both bounds
173+
keeps the schema symmetric with the prompt's stated count and prevents the
174+
model from over-generating in the (unlikely) inverse failure mode.
169175
"""
170176
item_schema = SEED_SCHEMA_WITH_TOOLS if tool_source == TOOL_SOURCE_PER_SEED else SEED_SCHEMA
177+
resolved_max = max_items if (max_items is not None and max_items > 0) else 2000
171178
seeds_schema: dict[str, Any] = {
172179
"type": "array",
173-
"maxItems": 2000,
180+
"maxItems": resolved_max,
174181
"items": item_schema,
175182
}
176183
if min_items is not None and min_items > 0:
@@ -663,7 +670,9 @@ async def _process(job: SeedJob) -> dict[str, Any]:
663670
)
664671
slug = slugify(str(job.behavior.get("name") or ""))
665672
behavior_name = str(job.behavior.get("name") or "")
666-
job_schema = seeds_response_schema(tool_source, min_items=job.count)
673+
job_schema = seeds_response_schema(
674+
tool_source, min_items=job.count, max_items=job.count
675+
)
667676

668677
response = await generate_structured(
669678
model,

tests/test_seed_sampling_characterization.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def test_schema_wraps_seeds_array(self) -> None:
135135
def test_schema_omits_min_items_by_default(self) -> None:
136136
schema = seeds_response_schema()
137137
self.assertNotIn("minItems", schema["properties"]["seeds"])
138+
self.assertEqual(schema["properties"]["seeds"]["maxItems"], 2000)
138139

139140
def test_schema_pins_min_items_when_count_supplied(self) -> None:
140141
schema = seeds_response_schema(min_items=27)
@@ -146,6 +147,16 @@ def test_schema_ignores_non_positive_min_items(self) -> None:
146147
schema = seeds_response_schema(min_items=value)
147148
self.assertNotIn("minItems", schema["properties"]["seeds"])
148149

150+
def test_schema_pins_both_bounds_when_min_and_max_match(self) -> None:
151+
schema = seeds_response_schema(min_items=500, max_items=500)
152+
self.assertEqual(schema["properties"]["seeds"]["minItems"], 500)
153+
self.assertEqual(schema["properties"]["seeds"]["maxItems"], 500)
154+
155+
def test_schema_ignores_non_positive_max_items(self) -> None:
156+
for value in (0, -1):
157+
schema = seeds_response_schema(max_items=value)
158+
self.assertEqual(schema["properties"]["seeds"]["maxItems"], 2000)
159+
149160

150161
class LabelEntrySchemaTest(unittest.TestCase):
151162
def test_schema_enumerates_present_factors_only(self) -> None:

0 commit comments

Comments
 (0)