roadmodel 0.2.72__tar.gz → 0.2.74__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 (57) hide show
  1. {roadmodel-0.2.72 → roadmodel-0.2.74}/PKG-INFO +1 -1
  2. {roadmodel-0.2.72/roadmodel/data → roadmodel-0.2.74/docs}/catalog.json +1 -1
  3. {roadmodel-0.2.72 → roadmodel-0.2.74}/pyproject.toml +1 -1
  4. {roadmodel-0.2.72/docs → roadmodel-0.2.74/roadmodel/data}/catalog.json +1 -1
  5. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/__init__.py +1 -1
  6. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/recommend.py +13 -3
  7. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/scoring.py +58 -7
  8. {roadmodel-0.2.72 → roadmodel-0.2.74}/.gitignore +0 -0
  9. {roadmodel-0.2.72 → roadmodel-0.2.74}/LICENSE +0 -0
  10. {roadmodel-0.2.72 → roadmodel-0.2.74}/NOTICE +0 -0
  11. {roadmodel-0.2.72 → roadmodel-0.2.74}/README.md +0 -0
  12. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/benchmarks.json +0 -0
  13. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/context-windows.json +0 -0
  14. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/model-selector.txt +0 -0
  15. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/model-tier-cost-scale.md +0 -0
  16. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/settings-display.md +0 -0
  17. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/templates/phase-roadmap-template.md +0 -0
  18. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/templates/planning-kit-how-to-use.md +0 -0
  19. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/templates/project-roadmap-template.md +0 -0
  20. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/templates/prompt-phase-roadmap.md +0 -0
  21. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/templates/prompt-project-roadmap.md +0 -0
  22. {roadmodel-0.2.72 → roadmodel-0.2.74}/docs/user-context.example.md +0 -0
  23. {roadmodel-0.2.72 → roadmodel-0.2.74}/hatch_build.py +0 -0
  24. {roadmodel-0.2.72 → roadmodel-0.2.74}/infra/README.md +0 -0
  25. {roadmodel-0.2.72 → roadmodel-0.2.74}/infra/gcp-killswitch/README.md +0 -0
  26. {roadmodel-0.2.72 → roadmodel-0.2.74}/infra/supabase/README.md +0 -0
  27. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/benchmarks.json +0 -0
  28. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/context-windows.json +0 -0
  29. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/model-selector.txt +0 -0
  30. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/model-tier-cost-scale.md +0 -0
  31. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/phase-roadmap-template.md +0 -0
  32. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/planning-kit-how-to-use.md +0 -0
  33. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/project-roadmap-template.md +0 -0
  34. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/prompt-phase-roadmap.md +0 -0
  35. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/prompt-project-roadmap.md +0 -0
  36. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/settings-display.md +0 -0
  37. {roadmodel-0.2.72 → roadmodel-0.2.74}/roadmodel/data/user-context.example.md +0 -0
  38. {roadmodel-0.2.72 → roadmodel-0.2.74}/scripts/agent_context.py +0 -0
  39. {roadmodel-0.2.72 → roadmodel-0.2.74}/scripts/agent_rules.py +0 -0
  40. {roadmodel-0.2.72 → roadmodel-0.2.74}/scripts/update_projects.py +0 -0
  41. {roadmodel-0.2.72 → roadmodel-0.2.74}/scripts/updater_lock.py +0 -0
  42. {roadmodel-0.2.72 → roadmodel-0.2.74}/scripts/upgrade_fleet.py +0 -0
  43. {roadmodel-0.2.72 → roadmodel-0.2.74}/scripts/worktree_memory.py +0 -0
  44. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/__main__.py +0 -0
  45. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/cli.py +0 -0
  46. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/config.py +0 -0
  47. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/cost.py +0 -0
  48. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/errors.py +0 -0
  49. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/mcp_server.py +0 -0
  50. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/providers/__init__.py +0 -0
  51. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/providers/anthropic.py +0 -0
  52. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/providers/google.py +0 -0
  53. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/providers/openai.py +0 -0
  54. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/providers/openai_compatible.py +0 -0
  55. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/providers/registry.py +0 -0
  56. {roadmodel-0.2.72 → roadmodel-0.2.74}/src/roadmodel/usage.py +0 -0
  57. {roadmodel-0.2.72 → roadmodel-0.2.74}/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.72
3
+ Version: 0.2.74
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "4",
3
3
  "output_contract_version": 2,
4
- "generated_at_utc": "2026-10-07T18:08:11Z",
4
+ "generated_at_utc": "2026-10-07T20:14:28Z",
5
5
  "source_doc_sha256": {
6
6
  "model-selector.txt": "6a75e2265459fa7302cb80d0faf5ec2aad575034fc29866238cd1579c001cc2d",
7
7
  "model-tier-cost-scale.md": "2b3b0eb82237eb5cb5bf468a3fe971bcaf0160ad7d0bc700698d2c16640bc41d"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "roadmodel"
7
- version = "0.2.72"
7
+ version = "0.2.74"
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"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "4",
3
3
  "output_contract_version": 2,
4
- "generated_at_utc": "2026-10-07T18:08:11Z",
4
+ "generated_at_utc": "2026-10-07T20:14:28Z",
5
5
  "source_doc_sha256": {
6
6
  "model-selector.txt": "6a75e2265459fa7302cb80d0faf5ec2aad575034fc29866238cd1579c001cc2d",
7
7
  "model-tier-cost-scale.md": "2b3b0eb82237eb5cb5bf468a3fe971bcaf0160ad7d0bc700698d2c16640bc41d"
@@ -1,2 +1,2 @@
1
1
  # src/roadmodel/__init__.py
2
- __version__ = "0.2.72"
2
+ __version__ = "0.2.74"
@@ -479,7 +479,11 @@ _SAAS_LADDER_TABLE_HEADER: Final = (
479
479
  "1. One line `CLASSIFICATION: <category> / <complexity> / <novel|routine>`. "
480
480
  "<category> is the task's PRIMARY category (coding, planning, agentic, multimodal, "
481
481
  "long-context, knowledge or speed) per Step 1 of the selection algorithm; "
482
- "<complexity> is low, medium or high per its Step 2. `novel` marks "
482
+ "<complexity> is low, medium or high per its Step 2. High-volume routine work "
483
+ "(classifying, tagging, extracting or summarizing many similar items) is `speed` "
484
+ "whatever accuracy it needs, and its complexity is how hard ONE item is. Building an "
485
+ "agent or an app (writing its code) is `coding`; running one, where the model itself "
486
+ "acts through tools in a loop, is `agentic`. `novel` marks "
483
487
  "research-grade work only: an open problem, a new algorithm, or a proof of a "
484
488
  "result with no known proof. Writing out a known result (a textbook proof, a "
485
489
  "standard derivation) is `routine` however many steps it takes, and its "
@@ -1487,12 +1491,18 @@ def _fit_row_to_input(
1487
1491
  """The row with every model whose context window cannot hold the task's
1488
1492
  material left out (scoring.too_small_for), and what the check found. The
1489
1493
  table is computed before the engine reads the task, so the row is
1490
- recomputed here when a model its frontier draws on is too small; with
1491
- none left that fits, the row stands and ``fits`` says so."""
1494
+ recomputed here when a model its frontier draws on is too small. When the
1495
+ material is larger than every known window, or none left fits, the row
1496
+ stands and ``fits`` says so."""
1492
1497
  small = set(scoring.too_small_for(input_tokens))
1493
1498
  drawn = {p.candidate.model_id for p in [*row.frontier, *row.points]}
1494
1499
  excluded = sorted(small & drawn)
1495
1500
  report: dict[str, Any] = {"input_tokens": input_tokens, "excluded": excluded, "fits": True}
1501
+ if scoring.nothing_fits(input_tokens):
1502
+ # Larger than every known window: no pick holds it, and one whose
1503
+ # window is unknown would stand only by default.
1504
+ report["fits"] = False
1505
+ return row, report
1496
1506
  if not excluded:
1497
1507
  return row, report
1498
1508
  refit = scoring.ladder(
@@ -330,6 +330,14 @@ def context_windows() -> dict[str, int]:
330
330
  return {str(k): v for k, v in models.items() if isinstance(v, int) and v > 0}
331
331
 
332
332
 
333
+ def nothing_fits(input_tokens: int, windows: dict[str, int] | None = None) -> bool:
334
+ """True when no model with a known context window can hold
335
+ ``input_tokens`` of material: the material is larger than any model
336
+ takes, and a model whose window is unknown is no answer."""
337
+ known = windows if windows is not None else context_windows()
338
+ return bool(known) and len(too_small_for(input_tokens, known)) == len(known)
339
+
340
+
333
341
  def too_small_for(input_tokens: int, windows: dict[str, int] | None = None) -> list[str]:
334
342
  """The catalog ids whose context window cannot hold ``input_tokens`` of
335
343
  material with CONTEXT_HEADROOM to spare. A model with no known window is
@@ -953,10 +961,20 @@ def rank(
953
961
  catalog: dict[str, Any] | None = None,
954
962
  benchmarks: dict[str, Any] | None = None,
955
963
  top: int | None = None,
964
+ effort_aware: bool = True,
956
965
  ) -> Ranking:
957
- """Rank every reachable (model, platform) pair for ``task``."""
966
+ """Rank every reachable (model, platform) pair for ``task``.
967
+
968
+ With ``effort_aware`` (the default), a model AA has measured at several
969
+ efforts stands at the one that scores best for the task's posture, among
970
+ those it may run (:func:`effort_settings`), its quality, requirement
971
+ penalty (in the category and in general capability) and cost read there;
972
+ a model measured only at efforts the task does not run is left out. Off,
973
+ every model stands on its headline row at its posture's effort: the pool
974
+ :func:`ladder` expands itself."""
958
975
  cat = catalog if catalog is not None else _cost._load_catalog()
959
- bench = with_composites(benchmarks if benchmarks is not None else _load_benchmarks())
976
+ raw_bench = benchmarks if benchmarks is not None else _load_benchmarks()
977
+ bench = with_composites(raw_bench)
960
978
  text = user_context_text or ""
961
979
  headroom = consumption_headroom(text)
962
980
  pools = pool_states(text)
@@ -1092,6 +1110,28 @@ def rank(
1092
1110
  continue
1093
1111
  candidates.append(best_for_model)
1094
1112
 
1113
+ if effort_aware and candidates:
1114
+ settings = effort_settings(
1115
+ candidates, task, headroom, lam=lam, k=k, catalog=cat, benchmarks=raw_bench
1116
+ )
1117
+ best: dict[str, Candidate] = {}
1118
+ for c in settings:
1119
+ held = best.get(c.model_id)
1120
+ if held is None or (c.score, -c.effective_cost_usd) > (
1121
+ held.score,
1122
+ -held.effective_cost_usd,
1123
+ ):
1124
+ best[c.model_id] = c
1125
+ for c in candidates:
1126
+ if c.model_id not in best:
1127
+ excluded.append(
1128
+ {
1129
+ "model": c.model_id,
1130
+ "reason": "measured only at efforts this task does not run",
1131
+ }
1132
+ )
1133
+ candidates = [best[c.model_id] for c in candidates if c.model_id in best]
1134
+
1095
1135
  # Funded first, then score, then cheaper, then id — deterministic.
1096
1136
  candidates.sort(
1097
1137
  key=lambda c: (c.funding == "unfunded", -c.score, c.effective_cost_usd, c.model_id)
@@ -1396,7 +1436,8 @@ def _at_level(
1396
1436
  task: Task,
1397
1437
  bench: dict[str, Any],
1398
1438
  scales: tuple[EvidenceScale | None, EvidenceScale | None],
1399
- ranking: Ranking,
1439
+ lam: float,
1440
+ k: float,
1400
1441
  ) -> Candidate:
1401
1442
  """``c`` run at ``level`` (None: its headline row, at the posture's
1402
1443
  effort): quality from AA's row at that effort, and the requirement
@@ -1426,7 +1467,7 @@ def _at_level(
1426
1467
  mult = EFFORT_TOKEN_MULTIPLIER[level]
1427
1468
  eff_cost = c.blended_price_usd * c.scarcity * mult
1428
1469
  decades = math.log10(max(eff_cost, COST_FLOOR_USD) / COST_FLOOR_USD)
1429
- cost_pen = ranking.lam * ranking.k_points_per_decade * decades
1470
+ cost_pen = lam * k * decades
1430
1471
  nudge = 0.5 if task.category in AGENT_CATEGORIES and c.platform_id in AGENT_SURFACES else 0.0
1431
1472
  return replace(
1432
1473
  c,
@@ -1449,8 +1490,9 @@ def effort_settings(
1449
1490
  pool: list[Candidate],
1450
1491
  task: Task,
1451
1492
  headroom: str,
1452
- ranking: Ranking,
1453
1493
  *,
1494
+ lam: float,
1495
+ k: float,
1454
1496
  catalog: dict[str, Any],
1455
1497
  benchmarks: dict[str, Any],
1456
1498
  ) -> list[Candidate]:
@@ -1485,7 +1527,7 @@ def effort_settings(
1485
1527
  if headroom == "uncapped" and c.scarcity == 0:
1486
1528
  levels = levels[-1:] # a free path runs at its top effort
1487
1529
  out.extend(
1488
- _at_level(c, lv, model, task, bench, scales, ranking) for lv in (levels or [None])
1530
+ _at_level(c, lv, model, task, bench, scales, lam, k) for lv in (levels or [None])
1489
1531
  )
1490
1532
  return out
1491
1533
 
@@ -1550,6 +1592,7 @@ def ladder(
1550
1592
  unavailable_models=unavailable_models,
1551
1593
  catalog=cat,
1552
1594
  benchmarks=bench,
1595
+ effort_aware=False,
1553
1596
  )
1554
1597
  funded = [c for c in ranking.candidates if c.funding != "unfunded"]
1555
1598
  pool = funded or ranking.candidates
@@ -1559,7 +1602,15 @@ def ladder(
1559
1602
  headroom = consumption_headroom(user_context_text or "")
1560
1603
  # The picks are read off the same frontier drawn over each model at every
1561
1604
  # effort AA measured it at, so an effort is part of the pick it prices.
1562
- settings = effort_settings(pool, base, headroom, ranking, catalog=cat, benchmarks=bench)
1605
+ settings = effort_settings(
1606
+ pool,
1607
+ base,
1608
+ headroom,
1609
+ lam=ranking.lam,
1610
+ k=ranking.k_points_per_decade,
1611
+ catalog=cat,
1612
+ benchmarks=bench,
1613
+ )
1563
1614
  # Every model AA measured only at efforts this task does not run leaves
1564
1615
  # none: the picks then fall back to the models' frontier.
1565
1616
  points = frontier(settings, bench) or front
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes