Warn when a twin is validated on a superseded model#71
Conversation
A research-agent run validated a twin on openai:gpt-4.1 while the frontier was gpt-5.5 — its own plan even said "gpt-5.5", but it free-typed gpt-4.1 into --model and nothing flagged it. A weak model understates twin capability and makes the whole validation uninformative, yet the choice was invisible. zwill already defaults to gpt-5.5 when --model is omitted (parse_model_specs), so the risk is only an explicit weak model. For the validation-critical exports (twin-probability-job, rank-utility-twin-job, numeric-twin-job), edsl-export now: - names the model(s) the twin will run on in its output envelope (data.models), so the choice is reviewable at the decision point; and - emits a loud `superseded_twin_model` warning when a chosen model matches a superseded family (gpt-4*, gpt-3.5, claude-3/2, gemini-1.x, legacy completions), pointing at a current frontier model and noting the gpt-5.5 default. The marker list is advisory and never blocks — use an older model deliberately by ignoring the warning. Extracted superseded_twin_model_labels/‑warning as pure, tested helpers; documented the rule in the agent workflow guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds a guardrail to
Confidence Score: 4/5Safe to merge; the warning logic is advisory and never blocking, so the worst case for any false positive or future marker drift is an unwanted console warning, not broken exports or incorrect results. The core detection and warning path is correct and thoroughly tested. The only nit is that model_count and the new models list both land in the same output envelope for twin-validation targets, computed from different sources; a future builder that populates its EDSL model list differently could cause them to disagree silently. Everything else is clean. zwill/edsl_integration.py — specifically the relationship between model_count (from the serialised EDSL job dict) and the new models list (from a second parse_model_specs call) in export_data. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["cmd_edsl_export(args)"] --> B{args.target in\nTWIN_VALIDATION_TARGETS?}
B -- No --> C[Build export_dict\nno model labeling]
B -- Yes --> D["parse_model_specs(args)\n→ specs"]
D --> E["model_labels = [model_label(...)\n for each spec]"]
E --> F["superseded_twin_model_warning(specs)"]
F --> G{Any spec matches\nSUPERSEDED_TWIN_MODEL_MARKERS?}
G -- No --> H[No warning]
G -- Yes --> I["append superseded_twin_model\nwarning to warnings[]"]
H --> J["export_data includes\nmodels: model_labels"]
I --> J
C --> K["emit_raw_export(…, export_data, warnings)"]
J --> K
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["cmd_edsl_export(args)"] --> B{args.target in\nTWIN_VALIDATION_TARGETS?}
B -- No --> C[Build export_dict\nno model labeling]
B -- Yes --> D["parse_model_specs(args)\n→ specs"]
D --> E["model_labels = [model_label(...)\n for each spec]"]
E --> F["superseded_twin_model_warning(specs)"]
F --> G{Any spec matches\nSUPERSEDED_TWIN_MODEL_MARKERS?}
G -- No --> H[No warning]
G -- Yes --> I["append superseded_twin_model\nwarning to warnings[]"]
H --> J["export_data includes\nmodels: model_labels"]
I --> J
C --> K["emit_raw_export(…, export_data, warnings)"]
J --> K
Reviews (1): Last reviewed commit: "Warn when a twin is validated on a super..." | Re-trigger Greptile |
| export_data = { | ||
| "target": args.target, | ||
| "survey": args.survey, | ||
| "scenario_count": scenario_count, | ||
| "model_count": len(export_dict.get("models", []) or []) or None, | ||
| } | ||
| if model_labels is not None: | ||
| export_data["models"] = model_labels |
There was a problem hiding this comment.
model_count and models can coexist with potentially different counts
For twin-validation targets, export_data gets both model_count (derived from export_dict.get("models", []), i.e. the serialised EDSL Jobs dict) and models (derived from a fresh parse_model_specs(args) call). In practice these agree because every builder calls parse_model_specs(args) from the same args, but if a future builder sources its model list differently the two fields would diverge silently. Consider populating model_count from len(model_labels) when model_labels is available, so the envelope is internally consistent by construction.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| } | ||
|
|
||
|
|
||
|
|
||
| def build_edsl_agent_list_dict(survey_name: str, args: argparse.Namespace) -> dict[str, Any]: |
There was a problem hiding this comment.
Three blank lines between
superseded_twin_model_warning and build_edsl_agent_list_dict; PEP 8 uses two.
| } | |
| def build_edsl_agent_list_dict(survey_name: str, args: argparse.Namespace) -> dict[str, Any]: | |
| } | |
| def build_edsl_agent_list_dict(survey_name: str, args: argparse.Namespace) -> dict[str, Any]: |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Why
The research-agent run we've been dissecting validated its twin on
openai:gpt-4.1while the frontier wasgpt-5.5. Its own plan said "gpt-5.5" — then it free-typedgpt-4.1into--modeland nothing flagged it. A weak model understates twin capability and makes the whole validation uninformative (it looked like "twins barely beat random" when the real story may be "we used a weak model"). The choice was invisible in the output.What
zwill already defaults to
gpt-5.5when--modelis omitted (parse_model_specs), so it never silently falls to EDSL'sgpt-4odefault. The only real gap is an explicitly-chosen weak model. For the validation-critical exports (twin-probability-job,rank-utility-twin-job,numeric-twin-job),edsl-exportnow:data.models) — the choice is reviewable at the decision point, before spending anything;superseded_twin_modelwarning when a chosen model matches a superseded family (gpt-4*,gpt-3.5,claude-3/2,gemini-1.x, legacy completions), pointing at a current frontier model and noting thegpt-5.5default.Advisory, never blocking — ignore the warning to benchmark an older model deliberately.
Behavior (live-verified)
Testing
pytest— 252 passed (addstests/test_twin_model_guard.py: superseded detection, warning fires only for weak models, the default model isn't itself flagged, target set).ruffclean.Note: the
SUPERSEDED_TWIN_MODEL_MARKERSlist is a deny-list of clearly-older families and should be kept current as models evolve.🤖 Generated with Claude Code