ai: keep invalid planning results out of reusable cache
This commit is contained in:
parent
e9c901f177
commit
8a3f53082f
@ -91,7 +91,7 @@ class BatchClient:
|
|||||||
raise ValueError("approved runtime/model configuration mismatch")
|
raise ValueError("approved runtime/model configuration mismatch")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def run(self, envelope):
|
def run(self, envelope, validator=None):
|
||||||
"""Cache a FA01/synthetic request by source, prompt, schema and model identity.
|
"""Cache a FA01/synthetic request by source, prompt, schema and model identity.
|
||||||
|
|
||||||
The roster application must filter source rows and permitted fields before
|
The roster application must filter source rows and permitted fields before
|
||||||
@ -118,6 +118,7 @@ class BatchClient:
|
|||||||
self._verify(record["result"], payload)
|
self._verify(record["result"], payload)
|
||||||
if record.get("cache_key") != key:
|
if record.get("cache_key") != key:
|
||||||
raise ValueError("cache identity mismatch")
|
raise ValueError("cache identity mismatch")
|
||||||
|
self._validate_output(destination, record, validator)
|
||||||
return {"cache_hit": True, "path": str(destination), "record": record}
|
return {"cache_hit": True, "path": str(destination), "record": record}
|
||||||
self.catalog()
|
self.catalog()
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
@ -128,9 +129,21 @@ class BatchClient:
|
|||||||
"request_sha256": hashlib.sha256(canonical(payload)).hexdigest(),
|
"request_sha256": hashlib.sha256(canonical(payload)).hexdigest(),
|
||||||
"campaign_id": envelope["campaign_id"], "suite_id": envelope["suite_id"],
|
"campaign_id": envelope["campaign_id"], "suite_id": envelope["suite_id"],
|
||||||
"client_wall_seconds": round(time.monotonic() - started, 3), "result": result}
|
"client_wall_seconds": round(time.monotonic() - started, 3), "result": result}
|
||||||
|
self._validate_output(destination, record, validator)
|
||||||
self._save(destination, record)
|
self._save(destination, record)
|
||||||
return {"cache_hit": False, "path": str(destination), "record": record}
|
return {"cache_hit": False, "path": str(destination), "record": record}
|
||||||
|
|
||||||
|
def _validate_output(self, destination, record, validator):
|
||||||
|
"""Keep failed application contracts as diagnostics, outside reusable cache."""
|
||||||
|
if validator is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
validator(json.loads(record["result"]["response"]))
|
||||||
|
except (ValueError, KeyError, TypeError):
|
||||||
|
self._save(destination.with_suffix(".rejected.json"), record)
|
||||||
|
destination.unlink(missing_ok=True)
|
||||||
|
raise ValueError("application validation failed; local diagnostic retained") from None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _verify(result, payload):
|
def _verify(result, payload):
|
||||||
"""Reject changed models, settings and incomplete results before caching."""
|
"""Reject changed models, settings and incomplete results before caching."""
|
||||||
|
|||||||
@ -64,16 +64,17 @@ def main():
|
|||||||
parser.error("case-id must select one bundled synthetic case")
|
parser.error("case-id must select one bundled synthetic case")
|
||||||
request = envelope(args.model, args.mode, sources, args.think)
|
request = envelope(args.model, args.mode, sources, args.think)
|
||||||
client = BatchClient(args.token_file, args.cache_dir)
|
client = BatchClient(args.token_file, args.cache_dir)
|
||||||
outcome = client.run(request)
|
def validate(content):
|
||||||
|
if args.mode == "profile":
|
||||||
|
validate_profile(content, sources[0])
|
||||||
|
elif args.mode == "group":
|
||||||
|
validate_families(content, sources)
|
||||||
|
elif content != {"status": "LOCAL_OK"}:
|
||||||
|
raise ValueError("synthetic transport check failed")
|
||||||
|
|
||||||
|
outcome = client.run(request, validator=validate)
|
||||||
record = outcome["record"]
|
record = outcome["record"]
|
||||||
result = record["result"]
|
result = record["result"]
|
||||||
content = json.loads(result["response"])
|
|
||||||
if args.mode == "profile":
|
|
||||||
validate_profile(content, sources[0])
|
|
||||||
elif args.mode == "group":
|
|
||||||
validate_families(content, sources)
|
|
||||||
elif content != {"status": "LOCAL_OK"}:
|
|
||||||
raise ValueError("synthetic transport check failed")
|
|
||||||
print(json.dumps({"synthetic": True, "mode": args.mode, "model": args.model,
|
print(json.dumps({"synthetic": True, "mode": args.mode, "model": args.model,
|
||||||
"cache_hit": outcome["cache_hit"], "result_file": outcome["path"],
|
"cache_hit": outcome["cache_hit"], "result_file": outcome["path"],
|
||||||
"wall_seconds": record["client_wall_seconds"],
|
"wall_seconds": record["client_wall_seconds"],
|
||||||
|
|||||||
@ -128,3 +128,17 @@ def test_redirects_and_shared_token_files_are_rejected(tmp_path):
|
|||||||
token.chmod(0o644)
|
token.chmod(0o644)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
client.BatchClient(token, tmp_path / "cache")
|
client.BatchClient(token, tmp_path / "cache")
|
||||||
|
|
||||||
|
|
||||||
|
def test_application_validation_failure_keeps_diagnostics_but_is_not_reused(transport, envelope):
|
||||||
|
instance, calls = transport
|
||||||
|
|
||||||
|
def reject(content):
|
||||||
|
raise ValueError("missing case IDs")
|
||||||
|
|
||||||
|
for _ in range(2):
|
||||||
|
with pytest.raises(ValueError, match="application validation failed"):
|
||||||
|
instance.run(envelope, validator=reject)
|
||||||
|
assert calls.count("/generate") == 2
|
||||||
|
assert len(list(instance.cache_dir.glob("*.rejected.json"))) == 1
|
||||||
|
assert len(list(instance.cache_dir.glob("*.json"))) == 1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user