roadmodel 0.2.84__tar.gz → 0.2.85__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {roadmodel-0.2.84 → roadmodel-0.2.85}/PKG-INFO +2 -2
  2. {roadmodel-0.2.84 → roadmodel-0.2.85}/README.md +1 -1
  3. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/claude-commands/roadmap-refresh.md +9 -2
  4. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/templates/prompt-phase-roadmap.md +8 -1
  5. {roadmodel-0.2.84 → roadmodel-0.2.85}/pyproject.toml +1 -1
  6. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/prompt-phase-roadmap.md +8 -1
  7. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/roadmap-refresh.md +9 -2
  8. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/__init__.py +1 -1
  9. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/cli.py +32 -2
  10. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/scoring.py +168 -25
  11. {roadmodel-0.2.84 → roadmodel-0.2.85}/.github/README.md +0 -0
  12. {roadmodel-0.2.84 → roadmodel-0.2.85}/.gitignore +0 -0
  13. {roadmodel-0.2.84 → roadmodel-0.2.85}/LICENSE +0 -0
  14. {roadmodel-0.2.84 → roadmodel-0.2.85}/NOTICE +0 -0
  15. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/benchmarks.json +0 -0
  16. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/catalog.json +0 -0
  17. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/claude-commands/roadmap-phase.md +0 -0
  18. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/claude-commands/roadmap-project.md +0 -0
  19. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/claude-commands/roadmap-step.md +0 -0
  20. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/claude-commands/roadmodel-upgrade.md +0 -0
  21. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/context-windows.json +0 -0
  22. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/effort-tokens.json +0 -0
  23. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/model-selector.txt +0 -0
  24. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/model-tier-cost-scale.md +0 -0
  25. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/settings-display.md +0 -0
  26. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/templates/phase-roadmap-template.md +0 -0
  27. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/templates/planning-kit-how-to-use.md +0 -0
  28. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/templates/project-roadmap-template.md +0 -0
  29. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/templates/prompt-project-roadmap.md +0 -0
  30. {roadmodel-0.2.84 → roadmodel-0.2.85}/docs/user-context.example.md +0 -0
  31. {roadmodel-0.2.84 → roadmodel-0.2.85}/hatch_build.py +0 -0
  32. {roadmodel-0.2.84 → roadmodel-0.2.85}/infra/README.md +0 -0
  33. {roadmodel-0.2.84 → roadmodel-0.2.85}/infra/gcp-killswitch/README.md +0 -0
  34. {roadmodel-0.2.84 → roadmodel-0.2.85}/infra/supabase/README.md +0 -0
  35. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/benchmarks.json +0 -0
  36. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/catalog.json +0 -0
  37. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/context-windows.json +0 -0
  38. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/effort-tokens.json +0 -0
  39. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/model-selector.txt +0 -0
  40. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/model-tier-cost-scale.md +0 -0
  41. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/phase-roadmap-template.md +0 -0
  42. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/planning-kit-how-to-use.md +0 -0
  43. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/project-roadmap-template.md +0 -0
  44. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/prompt-project-roadmap.md +0 -0
  45. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/roadmap-phase.md +0 -0
  46. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/roadmap-project.md +0 -0
  47. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/roadmap-step.md +0 -0
  48. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/roadmodel-upgrade.md +0 -0
  49. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/settings-display.md +0 -0
  50. {roadmodel-0.2.84 → roadmodel-0.2.85}/roadmodel/data/user-context.example.md +0 -0
  51. {roadmodel-0.2.84 → roadmodel-0.2.85}/scripts/agent_context.py +0 -0
  52. {roadmodel-0.2.84 → roadmodel-0.2.85}/scripts/agent_rules.py +0 -0
  53. {roadmodel-0.2.84 → roadmodel-0.2.85}/scripts/update_projects.py +0 -0
  54. {roadmodel-0.2.84 → roadmodel-0.2.85}/scripts/updater_lock.py +0 -0
  55. {roadmodel-0.2.84 → roadmodel-0.2.85}/scripts/upgrade_fleet.py +0 -0
  56. {roadmodel-0.2.84 → roadmodel-0.2.85}/scripts/worktree_memory.py +0 -0
  57. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/__main__.py +0 -0
  58. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/config.py +0 -0
  59. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/cost.py +0 -0
  60. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/errors.py +0 -0
  61. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/mcp_server.py +0 -0
  62. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/private_files.py +0 -0
  63. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/providers/__init__.py +0 -0
  64. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/providers/_safety.py +0 -0
  65. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/providers/anthropic.py +0 -0
  66. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/providers/google.py +0 -0
  67. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/providers/openai.py +0 -0
  68. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/providers/openai_compatible.py +0 -0
  69. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/providers/registry.py +0 -0
  70. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/recommend.py +0 -0
  71. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/usage.py +0 -0
  72. {roadmodel-0.2.84 → roadmodel-0.2.85}/src/roadmodel/user_context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: roadmodel
3
- Version: 0.2.84
3
+ Version: 0.2.85
4
4
  Summary: BYO-key CLI that recommends the right AI model, platform, and settings for a prompt.
5
5
  Project-URL: Homepage, https://roadmodel.ai
6
6
  Project-URL: Repository, https://github.com/nathanramoscfa/roadmodel
@@ -607,7 +607,7 @@ server and the reasoning-effort default in parity automatically.
607
607
  | `roadmodel recommend --model` | Override the specific model ID on the chosen provider. |
608
608
  | `roadmodel recommend --user-context PATH` | Override the user-context.md location for this invocation. |
609
609
  | `roadmodel score --category C --complexity X` | Rank (model, platform, effort) candidates in code for a classified task — no engine call. See [docs/scoring-model.md](docs/scoring-model.md). |
610
- | `roadmodel backup --category C --complexity X --model M --effort E` | Name the cross-maker backup for a pick, by the rule /recommend uses — no engine call. Prints the roadmap Settings Backup cell. |
610
+ | `roadmodel backup --category C --complexity X --model M --effort E` | Name the cross-maker backup for a pick, by the rule /recommend uses — no engine call. Prints the roadmap Settings Backup cell, and first a `MODEL` cell when a newer version of the pick's model (same platform, no dearer) scores level or better at its effort. |
611
611
  | `roadmodel catalog show` | Print the bundled `model-selector.txt` (use `--doc tier-cost-scale` for the price doc). |
612
612
  | `roadmodel catalog path` | Print the on-disk path of the bundled catalog document (same `--doc` flag). |
613
613
  | `roadmodel context show` | Print the resolved `user-context.md` file. |
@@ -356,7 +356,7 @@ server and the reasoning-effort default in parity automatically.
356
356
  | `roadmodel recommend --model` | Override the specific model ID on the chosen provider. |
357
357
  | `roadmodel recommend --user-context PATH` | Override the user-context.md location for this invocation. |
358
358
  | `roadmodel score --category C --complexity X` | Rank (model, platform, effort) candidates in code for a classified task — no engine call. See [docs/scoring-model.md](docs/scoring-model.md). |
359
- | `roadmodel backup --category C --complexity X --model M --effort E` | Name the cross-maker backup for a pick, by the rule /recommend uses — no engine call. Prints the roadmap Settings Backup cell. |
359
+ | `roadmodel backup --category C --complexity X --model M --effort E` | Name the cross-maker backup for a pick, by the rule /recommend uses — no engine call. Prints the roadmap Settings Backup cell, and first a `MODEL` cell when a newer version of the pick's model (same platform, no dearer) scores level or better at its effort. |
360
360
  | `roadmodel catalog show` | Print the bundled `model-selector.txt` (use `--doc tier-cost-scale` for the price doc). |
361
361
  | `roadmodel catalog path` | Print the on-disk path of the bundled catalog document (same `--doc` flag). |
362
362
  | `roadmodel context show` | Print the resolved `user-context.md` file. |
@@ -186,8 +186,15 @@ exclusion in the selector, and include a backup model.
186
186
  --effort <the step's effort>` prints for the step's classification —
187
187
  the rule /recommend backs a pick up by, which takes the cheaper of two
188
188
  substitutes the benchmarks cannot tell apart — and it outranks the
189
- selector's BACKUP. A table
190
- written before roadmodel 0.2.41 has no such row: add it from the
189
+ selector's BACKUP. When the command also prints a `MODEL` line, that
190
+ cell replaces the step's model — a newer version of the same model
191
+ (GPT-6 Sol → GPT-6.1 Sol), on the same platform, no dearer, that
192
+ scores level or better at the step's effort; the selector's letters
193
+ are measured at each model's top effort, and a step that runs lower
194
+ can reverse them. That is a difference: rewrite the table, its Model
195
+ rationale and its selection block with the `MODEL` cell, keep the
196
+ Backup cell printed with it, and add the `Settings updated` line.
197
+ A table written before roadmodel 0.2.41 has no such row: add it from the
191
198
  backup the step already names (its Model rationale's closing
192
199
  sentence, or its entry in the Model selection blocks) — a layout
193
200
  fix, not a new pick, so it takes no `Settings updated` line.
@@ -109,7 +109,14 @@ the cell `roadmodel backup --category <c> --complexity <x> [--novel]
109
109
  step's own classification: the rule /recommend backs a pick up by, in
110
110
  code, which takes the cheaper of two substitutes the benchmarks cannot
111
111
  tell apart. The selector's BACKUP stands only where it is not
112
- installed.
112
+ installed. When the command also prints a `MODEL` line, that cell
113
+ replaces the step's model: a newer version of the same model (GPT-6 Sol
114
+ → GPT-6.1 Sol), on the same platform, no dearer, that scores level or
115
+ better at the step's effort. The selector's letters are measured at each
116
+ model's top effort, and a step that runs lower can reverse them. Write
117
+ the `MODEL` cell into the Model row, the Model rationale and the step's
118
+ selection block, citing the newer model's figures at the step's effort,
119
+ and keep the Backup cell printed with it.
113
120
 
114
121
  Honor the template's style rules: 80-column prose, zero `{{...}}`
115
122
  tokens left, `**Status:** Not started` directly under the title and
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "roadmodel"
7
- version = "0.2.84"
7
+ version = "0.2.85"
8
8
  description = "BYO-key CLI that recommends the right AI model, platform, and settings for a prompt."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -109,7 +109,14 @@ the cell `roadmodel backup --category <c> --complexity <x> [--novel]
109
109
  step's own classification: the rule /recommend backs a pick up by, in
110
110
  code, which takes the cheaper of two substitutes the benchmarks cannot
111
111
  tell apart. The selector's BACKUP stands only where it is not
112
- installed.
112
+ installed. When the command also prints a `MODEL` line, that cell
113
+ replaces the step's model: a newer version of the same model (GPT-6 Sol
114
+ → GPT-6.1 Sol), on the same platform, no dearer, that scores level or
115
+ better at the step's effort. The selector's letters are measured at each
116
+ model's top effort, and a step that runs lower can reverse them. Write
117
+ the `MODEL` cell into the Model row, the Model rationale and the step's
118
+ selection block, citing the newer model's figures at the step's effort,
119
+ and keep the Backup cell printed with it.
113
120
 
114
121
  Honor the template's style rules: 80-column prose, zero `{{...}}`
115
122
  tokens left, `**Status:** Not started` directly under the title and
@@ -186,8 +186,15 @@ exclusion in the selector, and include a backup model.
186
186
  --effort <the step's effort>` prints for the step's classification —
187
187
  the rule /recommend backs a pick up by, which takes the cheaper of two
188
188
  substitutes the benchmarks cannot tell apart — and it outranks the
189
- selector's BACKUP. A table
190
- written before roadmodel 0.2.41 has no such row: add it from the
189
+ selector's BACKUP. When the command also prints a `MODEL` line, that
190
+ cell replaces the step's model — a newer version of the same model
191
+ (GPT-6 Sol → GPT-6.1 Sol), on the same platform, no dearer, that
192
+ scores level or better at the step's effort; the selector's letters
193
+ are measured at each model's top effort, and a step that runs lower
194
+ can reverse them. That is a difference: rewrite the table, its Model
195
+ rationale and its selection block with the `MODEL` cell, keep the
196
+ Backup cell printed with it, and add the `Settings updated` line.
197
+ A table written before roadmodel 0.2.41 has no such row: add it from the
191
198
  backup the step already names (its Model rationale's closing
192
199
  sentence, or its entry in the Model selection blocks) — a layout
193
200
  fix, not a new pick, so it takes no `Settings updated` line.
@@ -1,2 +1,2 @@
1
1
  # src/roadmodel/__init__.py
2
- __version__ = "0.2.84"
2
+ __version__ = "0.2.85"
@@ -561,17 +561,41 @@ def backup_command(
561
561
  The rule /recommend backs each pick up by: another maker's model that does
562
562
  the job as well, for the least; where the benchmarks cannot tell two
563
563
  substitutes apart, the cheaper. Prints the roadmap Settings Backup cell.
564
+
565
+ When a newer version of the pick's model (same maker and series, on the
566
+ same platform, no dearer) scores level or better at the pick's effort, it
567
+ first prints that model on a MODEL line: the step runs it instead, and the
568
+ backup printed is its backup.
564
569
  """
565
570
  text = _scoring_user_context(user_context_path)
566
571
  posture = (budget or scoring.declared_budget(text)).lower()
567
572
  try:
568
573
  task = scoring.Task(category.lower(), complexity.lower(), novel, posture)
569
- backup = scoring.backup_for_pick(task, text, model, effort)
574
+ successor = scoring.successor_for_pick(task, text, model, effort)
575
+ runs = successor.candidate.model_id if successor is not None else model
576
+ backup = scoring.backup_for_pick(task, text, runs, effort)
570
577
  except ValueError as exc:
571
578
  raise click.ClickException(str(exc)) from exc
572
579
  cell = scoring.backup_cell(backup) if backup is not None else "None"
580
+ model_cell = scoring.backup_cell(successor) if successor is not None else None
573
581
  if output.lower() == "json":
574
- payload: dict[str, Any] = {"backup": None, "cell": cell}
582
+ payload: dict[str, Any] = {
583
+ "model": None,
584
+ "model_cell": model_cell,
585
+ "backup": None,
586
+ "cell": cell,
587
+ }
588
+ if successor is not None:
589
+ s = successor.candidate
590
+ payload["model"] = {
591
+ "model_id": s.model_id,
592
+ "model": s.model_name,
593
+ "platform_id": s.platform_id,
594
+ "platform": s.platform_name,
595
+ "effort": successor.effort,
596
+ "native_effort": successor.native,
597
+ "replaces": successor.replaces,
598
+ }
575
599
  if backup is not None:
576
600
  c = backup.candidate
577
601
  payload["backup"] = {
@@ -584,6 +608,12 @@ def backup_command(
584
608
  }
585
609
  click.echo(json.dumps(payload, indent=2, ensure_ascii=False))
586
610
  return
611
+ if successor is not None:
612
+ click.echo(f"MODEL {model_cell}")
613
+ click.echo(
614
+ f" replaces {successor.replaces}: a newer version, no dearer, "
615
+ "level or better at this effort"
616
+ )
587
617
  click.echo(f"BACKUP {cell}")
588
618
 
589
619
 
@@ -2027,22 +2027,49 @@ def _model_ref_id(ref: str, catalog: dict[str, Any]) -> str:
2027
2027
  return ref
2028
2028
 
2029
2029
 
2030
- def backup_for_pick(
2030
+ @dataclass(frozen=True)
2031
+ class _Pick:
2032
+ """A pick made outside the ladder (a roadmap step's Settings), with the
2033
+ pool and scales it is read against (:func:`_pick`)."""
2034
+
2035
+ candidate: Candidate # the model, on the platform the pool reaches it best
2036
+ level: str # the scorer's level it runs at
2037
+ ranking: Ranking
2038
+ base: Task
2039
+ models: dict[str, dict[str, Any]]
2040
+ bench: dict[str, Any]
2041
+ composite: dict[str, Any]
2042
+ scales: tuple[EvidenceScale | None, EvidenceScale | None]
2043
+ catalog: dict[str, Any]
2044
+
2045
+ def at(self, c: Candidate, level: str | None) -> Candidate:
2046
+ """``c`` read at ``level`` (None: its headline row; see :func:`_at_level`)."""
2047
+ return _at_level(
2048
+ c,
2049
+ level,
2050
+ self.models[c.model_id],
2051
+ self.base,
2052
+ self.composite,
2053
+ self.scales,
2054
+ self.ranking.lam,
2055
+ self.ranking.k_points_per_decade,
2056
+ )
2057
+
2058
+ def measured(self, model_id: str) -> bool:
2059
+ return self.level in measured_levels(self.bench, model_id)
2060
+
2061
+
2062
+ def _pick(
2031
2063
  task: Task,
2032
2064
  user_context_text: str,
2033
2065
  model: str,
2034
2066
  effort: str,
2035
- *,
2036
- unavailable_models: list[str] | None = None,
2037
- catalog: dict[str, Any] | None = None,
2038
- benchmarks: dict[str, Any] | None = None,
2039
- ) -> Backup | None:
2040
- """The backup for a pick made outside the ladder (a roadmap step's
2041
- Settings): ``model`` (catalog id or name) run at ``effort`` (a scorer or
2042
- native level), backed up by the rule a /recommend rung follows
2043
- (:func:`_backup_for`), over the same pool, so the two never disagree.
2044
- ``task.budget`` sets the posture an unmeasured backup's effort follows.
2045
- None when the pool holds no other maker's model. ValueError when the
2067
+ unavailable_models: list[str] | None,
2068
+ catalog: dict[str, Any] | None,
2069
+ benchmarks: dict[str, Any] | None,
2070
+ ) -> _Pick:
2071
+ """``model`` (catalog id or name) run at ``effort`` (a scorer or native
2072
+ level) for ``task``, over the pool /recommend ranks. ValueError when the
2046
2073
  model is not one this user-context can reach."""
2047
2074
  cat = catalog if catalog is not None else _cost._load_catalog()
2048
2075
  bench = benchmarks if benchmarks is not None else _load_benchmarks()
@@ -2060,30 +2087,145 @@ def backup_for_pick(
2060
2087
  models = {str(m.get("id", "")): m for m in cat.get("models", []) if isinstance(m, dict)}
2061
2088
  if picked is None or model_id not in models:
2062
2089
  raise ValueError(f"{model!r} is not a model this user-context can reach")
2063
- funded = [c for c in ranking.candidates if c.funding != "unfunded"]
2064
- pool = funded or ranking.candidates
2065
- headroom = consumption_headroom(user_context_text or "")
2066
- lam, k = ranking.lam, ranking.k_points_per_decade
2067
- settings = effort_settings(pool, base, headroom, lam=lam, k=k, catalog=cat, benchmarks=bench)
2068
2090
  composite = with_composites(bench)
2069
2091
  scales = (
2070
2092
  _evidence_scale(cat, composite, CATEGORY_EVIDENCE[base.category]),
2071
2093
  _evidence_scale(cat, composite, CATEGORY_EVIDENCE["planning"]),
2072
2094
  )
2073
- level = _scorer_level(effort)
2074
- measured = level in measured_levels(bench, model_id)
2075
- at = _at_level(
2076
- picked, level if measured else None, models[model_id], base, composite, scales, lam, k
2095
+ return _Pick(
2096
+ picked, _scorer_level(effort), ranking, base, models, bench, composite, scales, cat
2097
+ )
2098
+
2099
+
2100
+ def backup_for_pick(
2101
+ task: Task,
2102
+ user_context_text: str,
2103
+ model: str,
2104
+ effort: str,
2105
+ *,
2106
+ unavailable_models: list[str] | None = None,
2107
+ catalog: dict[str, Any] | None = None,
2108
+ benchmarks: dict[str, Any] | None = None,
2109
+ ) -> Backup | None:
2110
+ """The backup for a pick made outside the ladder (a roadmap step's
2111
+ Settings): ``model`` (catalog id or name) run at ``effort`` (a scorer or
2112
+ native level), backed up by the rule a /recommend rung follows
2113
+ (:func:`_backup_for`), over the same pool, so the two never disagree.
2114
+ ``task.budget`` sets the posture an unmeasured backup's effort follows.
2115
+ None when the pool holds no other maker's model. ValueError when the
2116
+ model is not one this user-context can reach."""
2117
+ p = _pick(task, user_context_text, model, effort, unavailable_models, catalog, benchmarks)
2118
+ funded = [c for c in p.ranking.candidates if c.funding != "unfunded"]
2119
+ pool = funded or p.ranking.candidates
2120
+ headroom = consumption_headroom(user_context_text or "")
2121
+ settings = effort_settings(
2122
+ pool,
2123
+ p.base,
2124
+ headroom,
2125
+ lam=p.ranking.lam,
2126
+ k=p.ranking.k_points_per_decade,
2127
+ catalog=p.catalog,
2128
+ benchmarks=p.bench,
2077
2129
  )
2130
+ model_id = p.candidate.model_id
2131
+ at = p.at(p.candidate, p.level if p.measured(model_id) else None)
2078
2132
  point = FrontierPoint(
2079
- at, _evidence(bench, model_id, FRONTIER_INDEX, at.evidence_level), frontier_price(at)
2133
+ at, _evidence(p.bench, model_id, FRONTIER_INDEX, at.evidence_level), frontier_price(at)
2080
2134
  )
2081
2135
  tier = next(t for t, b in TIER_BUDGET.items() if b == task.budget)
2082
2136
  return _backup_for(
2083
- Rung(tier, point, level), settings, task, headroom, bench, cat, _aa_band(bench, cat)
2137
+ Rung(tier, point, p.level),
2138
+ settings,
2139
+ task,
2140
+ headroom,
2141
+ p.bench,
2142
+ p.catalog,
2143
+ _aa_band(p.bench, p.catalog),
2084
2144
  )
2085
2145
 
2086
2146
 
2147
+ def _series(model_id: str) -> frozenset[str]:
2148
+ """A model id's name words, its version numbers and the vendor prefix
2149
+ aside: `gpt-6-sol` and `gpt-6.1-sol` are one series, `gpt-6-sol` and
2150
+ `gpt-6-luna` two. Mirrors update/rating_guard.py same_series."""
2151
+ return frozenset(re.findall(r"[a-z]+", model_id.lower())) - {"claude"}
2152
+
2153
+
2154
+ def _version(model_id: str) -> tuple[int, ...]:
2155
+ return tuple(int(n) for n in re.findall(r"\d+", model_id))
2156
+
2157
+
2158
+ @dataclass(frozen=True)
2159
+ class Successor:
2160
+ """A newer version of a pick's model that runs in its place
2161
+ (:func:`successor_for_pick`)."""
2162
+
2163
+ candidate: Candidate # the newer model, read at the pick's effort
2164
+ effort: str # the scorer's level
2165
+ native: str | None
2166
+ replaces: str # the pick's model name
2167
+
2168
+ @property
2169
+ def level(self) -> str:
2170
+ return self.native or self.effort
2171
+
2172
+
2173
+ def successor_for_pick(
2174
+ task: Task,
2175
+ user_context_text: str,
2176
+ model: str,
2177
+ effort: str,
2178
+ *,
2179
+ unavailable_models: list[str] | None = None,
2180
+ catalog: dict[str, Any] | None = None,
2181
+ benchmarks: dict[str, Any] | None = None,
2182
+ ) -> Successor | None:
2183
+ """The newer version of ``model`` a pick made outside the ladder should run
2184
+ instead: the same maker's next release in the same series (GPT-6 Sol →
2185
+ GPT-6.1 Sol), funded on the pick's platform, no dearer per token, and, run
2186
+ at ``effort``, level with it in the task's category (:func:`_level_with`)
2187
+ and at least as high on the AA Intelligence Index. The newest such version;
2188
+ None when there is none. ValueError when ``model`` is not one this
2189
+ user-context can reach.
2190
+
2191
+ The selector reads each model's letters at its top effort, and a step that
2192
+ runs lower can reverse them: GPT-6 Sol letters S in coding and GPT-6.1 Sol
2193
+ A on a 3-point SciCode gap at max, yet at medium 6.1 Sol leads it on
2194
+ Terminal-Bench 4.0 by 29 points at the same price. The two are compared
2195
+ at ``effort`` where AA has measured both there, else on their headline
2196
+ rows, so neither is credited with a run the other is not."""
2197
+ p = _pick(task, user_context_text, model, effort, unavailable_models, catalog, benchmarks)
2198
+ old = p.candidate
2199
+ series, version = _series(old.model_id), _version(old.model_id)
2200
+ best: Successor | None = None
2201
+ for c in p.ranking.candidates:
2202
+ if (
2203
+ c.platform_id != old.platform_id
2204
+ or c.provider != old.provider
2205
+ or _series(c.model_id) != series
2206
+ or _version(c.model_id) <= version
2207
+ or c.blended_price_usd > old.blended_price_usd
2208
+ ):
2209
+ continue
2210
+ both = p.measured(old.model_id) and p.measured(c.model_id)
2211
+ level = p.level if both else None
2212
+ was, now = p.at(old, level), p.at(c, level)
2213
+ aa_was = _evidence(p.bench, old.model_id, FRONTIER_INDEX, level)
2214
+ aa_now = _evidence(p.bench, c.model_id, FRONTIER_INDEX, level)
2215
+ if not _level_with(now, was, task.category):
2216
+ continue
2217
+ if aa_now is None or (aa_was is not None and aa_now < aa_was):
2218
+ continue
2219
+ if best is not None and (
2220
+ _version(c.model_id),
2221
+ -c.blended_price_usd,
2222
+ ) <= (_version(best.candidate.model_id), -best.candidate.blended_price_usd):
2223
+ continue
2224
+ native = native_level(c.platform_id, c.model_id, p.level, catalog=p.catalog)
2225
+ best = Successor(now, p.level, native, old.model_name)
2226
+ return best
2227
+
2228
+
2087
2229
  # A platform's effort dial as a Settings table names it (docs/settings-display.md):
2088
2230
  # Intelligence on OpenAI's reasoning surfaces, Effort elsewhere; Cursor has none.
2089
2231
  _DIAL_NAMES: Final[dict[str, str | None]] = {
@@ -2103,9 +2245,10 @@ _LEVEL_WORDS: Final[dict[str, str]] = {
2103
2245
  }
2104
2246
 
2105
2247
 
2106
- def backup_cell(backup: Backup) -> str:
2248
+ def backup_cell(backup: Backup | Successor) -> str:
2107
2249
  """``backup`` as a roadmap Settings table's Backup cell:
2108
- ``GPT-6.1 Sol — Codex · Intelligence XHigh``."""
2250
+ ``GPT-6.1 Sol — Codex · Intelligence XHigh``. A :class:`Successor` reads
2251
+ the same way, as the model the step runs."""
2109
2252
  c = backup.candidate
2110
2253
  dial = _DIAL_NAMES.get(c.platform_id, "Effort")
2111
2254
  level = _LEVEL_WORDS.get(normalize_level(backup.level), backup.level)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes