roadmodel 0.2.62__tar.gz → 0.2.63__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.
- {roadmodel-0.2.62 → roadmodel-0.2.63}/PKG-INFO +1 -1
- {roadmodel-0.2.62 → roadmodel-0.2.63}/pyproject.toml +1 -1
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/__init__.py +1 -1
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/recommend.py +56 -6
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/scoring.py +99 -10
- {roadmodel-0.2.62 → roadmodel-0.2.63}/.gitignore +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/LICENSE +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/NOTICE +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/README.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/benchmarks.json +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/catalog.json +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/model-selector.txt +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/model-tier-cost-scale.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/settings-display.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/templates/phase-roadmap-template.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/templates/planning-kit-how-to-use.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/templates/project-roadmap-template.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/templates/prompt-phase-roadmap.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/templates/prompt-project-roadmap.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/docs/user-context.example.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/hatch_build.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/infra/README.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/infra/gcp-killswitch/README.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/infra/supabase/README.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/benchmarks.json +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/catalog.json +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/model-selector.txt +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/model-tier-cost-scale.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/phase-roadmap-template.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/planning-kit-how-to-use.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/project-roadmap-template.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/prompt-phase-roadmap.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/prompt-project-roadmap.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/settings-display.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/roadmodel/data/user-context.example.md +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/scripts/update_projects.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/__main__.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/cli.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/config.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/cost.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/errors.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/mcp_server.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/providers/__init__.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/providers/anthropic.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/providers/google.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/providers/openai.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/providers/openai_compatible.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/providers/registry.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/src/roadmodel/usage.py +0 -0
- {roadmodel-0.2.62 → roadmodel-0.2.63}/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.
|
|
3
|
+
Version: 0.2.63
|
|
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,2 +1,2 @@
|
|
|
1
1
|
# src/roadmodel/__init__.py
|
|
2
|
-
__version__ = "0.2.
|
|
2
|
+
__version__ = "0.2.63"
|
|
@@ -501,8 +501,10 @@ _SAAS_LADDER_TABLE_HEADER: Final = (
|
|
|
501
501
|
"gives both the same settings because a lower effort saves this user nothing or "
|
|
502
502
|
"the dial has no lower level; say which in that block's RATIONALE, and never "
|
|
503
503
|
"invent another difference.\n"
|
|
504
|
-
"- BACKUP
|
|
505
|
-
"
|
|
504
|
+
"- Each block's BACKUP is the model the row names after `backup` in that tier's "
|
|
505
|
+
"cell, copied verbatim; code checks it against the row and replaces one that "
|
|
506
|
+
"differs. Where a cell names no backup, BACKUP obeys the Step 7 HARD "
|
|
507
|
+
"cross-provider rule and names a model this user can reach.\n"
|
|
506
508
|
"- Each RATIONALE is three labelled segments (TASK: / PICK: / EFFORT:), one crisp "
|
|
507
509
|
"sentence each. PICK says why the row's model fits its tier in the table's terms "
|
|
508
510
|
"(where it sits on the user's frontier, its AA Index); EFFORT says why the setting "
|
|
@@ -1331,6 +1333,48 @@ def _enforce_table_row(ladder: dict[str, dict[str, str]], row: scoring.Ladder) -
|
|
|
1331
1333
|
return rewritten
|
|
1332
1334
|
|
|
1333
1335
|
|
|
1336
|
+
def _enforce_table_backup(ladder: dict[str, dict[str, str]], row: scoring.Ladder) -> list[str]:
|
|
1337
|
+
"""Make every rung's BACKUP the row's: the best model from another maker
|
|
1338
|
+
the user can run at no higher list price (scoring._backup_for). A rung
|
|
1339
|
+
whose row names no backup (no other maker funded) keeps the engine's,
|
|
1340
|
+
under the Step 7 guard. Returns the tiers whose backup it set."""
|
|
1341
|
+
adjusted: list[str] = []
|
|
1342
|
+
for tier in _LADDER_TIERS:
|
|
1343
|
+
backup = row.rungs[tier].backup
|
|
1344
|
+
if backup is None:
|
|
1345
|
+
continue
|
|
1346
|
+
b = backup.candidate
|
|
1347
|
+
written = ladder[tier].get("backup", "")
|
|
1348
|
+
if written and (
|
|
1349
|
+
cost.model_id_of(written) == b.model_id
|
|
1350
|
+
or _model_key(written) in {_model_key(b.model_id), _model_key(b.model_name)}
|
|
1351
|
+
):
|
|
1352
|
+
continue
|
|
1353
|
+
ladder[tier]["backup"] = b.model_name
|
|
1354
|
+
adjusted.append(tier)
|
|
1355
|
+
return adjusted
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def _attach_backup_plans(picks: dict[str, dict[str, Any]], row: scoring.Ladder) -> None:
|
|
1359
|
+
"""Give each pick whose BACKUP is the row's a ``backup_plan``: the
|
|
1360
|
+
platform and effort the row runs that backup at, so a caller can show the
|
|
1361
|
+
backup on the surface the scorer chose (the user's subscription surface
|
|
1362
|
+
where one reaches it) instead of guessing one. A backup the guard replaced
|
|
1363
|
+
gets none."""
|
|
1364
|
+
for tier in _LADDER_TIERS:
|
|
1365
|
+
backup = row.rungs[tier].backup
|
|
1366
|
+
if backup is None:
|
|
1367
|
+
continue
|
|
1368
|
+
b = backup.candidate
|
|
1369
|
+
if picks[tier].get("backup") != cost.canonical_model_name(b.model_name):
|
|
1370
|
+
continue
|
|
1371
|
+
picks[tier]["backup_plan"] = {
|
|
1372
|
+
"model": picks[tier]["backup"],
|
|
1373
|
+
"platform": b.platform_name,
|
|
1374
|
+
"effort": _effort_word(backup.level),
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
|
|
1334
1378
|
# Claude Code's Ultracode is a session setting above its Max level: it stands
|
|
1335
1379
|
# where the row says max.
|
|
1336
1380
|
_ULTRACODE_PLATFORM: Final = "claude-code"
|
|
@@ -1410,10 +1454,13 @@ def recommend_structured_ladder(
|
|
|
1410
1454
|
ladder table (scoring.ladder_table) goes into the prompt, the engine
|
|
1411
1455
|
classifies the task and copies its row, and every rung is then made the
|
|
1412
1456
|
row's (:func:`_enforce_table_row`), effort included on every surface
|
|
1413
|
-
whose dial the catalog documents (:func:`_enforce_table_effort`)
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1457
|
+
whose dial the catalog documents (:func:`_enforce_table_effort`), and
|
|
1458
|
+
BACKUP included where the row names one (:func:`_enforce_table_backup`;
|
|
1459
|
+
such a pick also carries a ``backup_plan`` with the backup's platform and
|
|
1460
|
+
effort). ``guard`` then reports ``mode: "frontier"``, the row, how it was
|
|
1461
|
+
found, the tiers rewritten, the tiers whose effort was set and the tiers
|
|
1462
|
+
whose backup was set, and is healthy: two rungs on one model are the
|
|
1463
|
+
table's answer, not a collapse.
|
|
1417
1464
|
Without a table, or a response the table cannot place, the engine's own
|
|
1418
1465
|
picks stand under the tier-distinctness guard as before.
|
|
1419
1466
|
|
|
@@ -1460,6 +1507,7 @@ def recommend_structured_ladder(
|
|
|
1460
1507
|
"found": found,
|
|
1461
1508
|
"rewritten": _enforce_table_row(ladder, row),
|
|
1462
1509
|
"effort_set": _enforce_table_effort(ladder, row),
|
|
1510
|
+
"backup_set": _enforce_table_backup(ladder, row),
|
|
1463
1511
|
"frontier": [p.candidate.model_name for p in row.frontier],
|
|
1464
1512
|
}
|
|
1465
1513
|
else:
|
|
@@ -1474,6 +1522,8 @@ def recommend_structured_ladder(
|
|
|
1474
1522
|
)
|
|
1475
1523
|
for tier in _LADDER_TIERS
|
|
1476
1524
|
}
|
|
1525
|
+
if frontier_guard is not None and frontier_guard["classification"] is not None:
|
|
1526
|
+
_attach_backup_plans(picks, table[frontier_guard["classification"]])
|
|
1477
1527
|
guard = _ladder_tier_guard(picks)
|
|
1478
1528
|
if frontier_guard is not None:
|
|
1479
1529
|
guard.update(frontier_guard)
|
|
@@ -56,7 +56,7 @@ from __future__ import annotations
|
|
|
56
56
|
import json
|
|
57
57
|
import math
|
|
58
58
|
import re
|
|
59
|
-
from dataclasses import asdict, dataclass, field
|
|
59
|
+
from dataclasses import asdict, dataclass, field, replace
|
|
60
60
|
from datetime import datetime, timedelta, timezone
|
|
61
61
|
from importlib import resources
|
|
62
62
|
from typing import Any, Final
|
|
@@ -1024,6 +1024,24 @@ class FrontierPoint:
|
|
|
1024
1024
|
price_usd: float
|
|
1025
1025
|
|
|
1026
1026
|
|
|
1027
|
+
@dataclass(frozen=True)
|
|
1028
|
+
class Backup:
|
|
1029
|
+
"""A rung's cross-maker substitute, read off the frontier of the other
|
|
1030
|
+
makers' models (see :func:`ladder`)."""
|
|
1031
|
+
|
|
1032
|
+
point: FrontierPoint
|
|
1033
|
+
effort: str
|
|
1034
|
+
native: str | None = None
|
|
1035
|
+
|
|
1036
|
+
@property
|
|
1037
|
+
def candidate(self) -> Candidate:
|
|
1038
|
+
return self.point.candidate
|
|
1039
|
+
|
|
1040
|
+
@property
|
|
1041
|
+
def level(self) -> str:
|
|
1042
|
+
return self.native or self.effort
|
|
1043
|
+
|
|
1044
|
+
|
|
1027
1045
|
@dataclass(frozen=True)
|
|
1028
1046
|
class Rung:
|
|
1029
1047
|
tier: str
|
|
@@ -1033,6 +1051,9 @@ class Rung:
|
|
|
1033
1051
|
# The same level as the platform's own dial names it (native_level), or
|
|
1034
1052
|
# None when the catalog documents no dial for the platform and model.
|
|
1035
1053
|
native: str | None = None
|
|
1054
|
+
# The best cross-maker substitute at no higher list price; None when the
|
|
1055
|
+
# pool holds no other maker's model.
|
|
1056
|
+
backup: Backup | None = None
|
|
1036
1057
|
|
|
1037
1058
|
@property
|
|
1038
1059
|
def candidate(self) -> Candidate:
|
|
@@ -1052,6 +1073,8 @@ class Ladder:
|
|
|
1052
1073
|
# complexity's requirement, cheapest first.
|
|
1053
1074
|
adequate: list[FrontierPoint]
|
|
1054
1075
|
rungs: dict[str, Rung]
|
|
1076
|
+
# Why a rung has no backup: the pool holds no other maker's model.
|
|
1077
|
+
backup_warning: str | None = None
|
|
1055
1078
|
|
|
1056
1079
|
def to_dict(self) -> dict[str, Any]:
|
|
1057
1080
|
def point(p: FrontierPoint) -> dict[str, Any]:
|
|
@@ -1072,9 +1095,23 @@ class Ladder:
|
|
|
1072
1095
|
"frontier": [point(p) for p in self.frontier],
|
|
1073
1096
|
"adequate": [p.candidate.model_id for p in self.adequate],
|
|
1074
1097
|
"rungs": {
|
|
1075
|
-
t: {
|
|
1098
|
+
t: {
|
|
1099
|
+
**point(r.point),
|
|
1100
|
+
"effort": r.effort,
|
|
1101
|
+
"native_effort": r.native,
|
|
1102
|
+
"backup": (
|
|
1103
|
+
{
|
|
1104
|
+
**point(r.backup.point),
|
|
1105
|
+
"effort": r.backup.effort,
|
|
1106
|
+
"native_effort": r.backup.native,
|
|
1107
|
+
}
|
|
1108
|
+
if r.backup is not None
|
|
1109
|
+
else None
|
|
1110
|
+
),
|
|
1111
|
+
}
|
|
1076
1112
|
for t, r in self.rungs.items()
|
|
1077
1113
|
},
|
|
1114
|
+
"backup_warning": self.backup_warning,
|
|
1078
1115
|
}
|
|
1079
1116
|
|
|
1080
1117
|
|
|
@@ -1129,6 +1166,9 @@ def ladder(
|
|
|
1129
1166
|
level they may converge. On an `uncapped` pool every free rung runs at
|
|
1130
1167
|
the top effort and two rungs on one model converge: a lower effort saves
|
|
1131
1168
|
that operator nothing.
|
|
1169
|
+
- Each rung carries a BACKUP from another maker (:func:`_backup_for`):
|
|
1170
|
+
the best adequate point on the other makers' frontier at no higher list
|
|
1171
|
+
price. ``backup_warning`` says why a rung has none.
|
|
1132
1172
|
|
|
1133
1173
|
None when no pool model carries an AA Index (no frontier to read)."""
|
|
1134
1174
|
cat = catalog if catalog is not None else _cost._load_catalog()
|
|
@@ -1183,8 +1223,52 @@ def ladder(
|
|
|
1183
1223
|
)
|
|
1184
1224
|
balanced_rung = _below(balanced_rung, quality_rung, cat)
|
|
1185
1225
|
cost_rung = _below(cost_rung, balanced_rung, cat)
|
|
1186
|
-
|
|
1187
|
-
|
|
1226
|
+
pool = funded or ranking.candidates
|
|
1227
|
+
rungs = {
|
|
1228
|
+
r.tier: replace(r, backup=_backup_for(r, pool, task, headroom, bench, cat))
|
|
1229
|
+
for r in (cost_rung, balanced_rung, quality_rung)
|
|
1230
|
+
}
|
|
1231
|
+
warning: str | None = None
|
|
1232
|
+
if any(r.backup is None for r in rungs.values()):
|
|
1233
|
+
makers = sorted({c.provider for c in pool})
|
|
1234
|
+
warning = (
|
|
1235
|
+
f"No cross-provider backup: the models this user can run come from "
|
|
1236
|
+
f"{', '.join(makers) or 'no maker'} only. Add a second maker's subscription or "
|
|
1237
|
+
"API key so an outage or an exhausted pool has somewhere to go."
|
|
1238
|
+
)
|
|
1239
|
+
return Ladder(task=base, frontier=front, adequate=adequate, rungs=rungs, backup_warning=warning)
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def _backup_for(
|
|
1243
|
+
rung: Rung,
|
|
1244
|
+
pool: list[Candidate],
|
|
1245
|
+
task: Task,
|
|
1246
|
+
headroom: str,
|
|
1247
|
+
bench: dict[str, Any],
|
|
1248
|
+
catalog: dict[str, Any],
|
|
1249
|
+
) -> Backup | None:
|
|
1250
|
+
"""The rung's backup: the best substitute from another maker the operator
|
|
1251
|
+
can run, at no higher list price.
|
|
1252
|
+
|
|
1253
|
+
Over the pool's models from a maker other than the rung model's, draw the
|
|
1254
|
+
frontier and keep its adequate points. The backup is the adequate point
|
|
1255
|
+
with the highest list price at or below the rung's; else the cheapest
|
|
1256
|
+
adequate point above it; else, with no adequate point, the frontier's
|
|
1257
|
+
strongest for the category. It runs at the rung's posture's effort on its
|
|
1258
|
+
own platform. None when the pool holds no other maker's model."""
|
|
1259
|
+
front = frontier([c for c in pool if c.provider != rung.candidate.provider], bench)
|
|
1260
|
+
if not front:
|
|
1261
|
+
return None
|
|
1262
|
+
adequate = [p for p in front if p.candidate.requirement_penalty == 0]
|
|
1263
|
+
if adequate:
|
|
1264
|
+
within = [p for p in adequate if p.price_usd <= rung.point.price_usd]
|
|
1265
|
+
chosen = within[-1] if within else adequate[0]
|
|
1266
|
+
else:
|
|
1267
|
+
chosen = max(front, key=lambda p: (p.candidate.quality, p.aa_index, -p.price_usd))
|
|
1268
|
+
t = Task(task.category, task.complexity, task.novel, TIER_BUDGET[rung.tier])
|
|
1269
|
+
c = chosen.candidate
|
|
1270
|
+
effort = effort_for(t, headroom, c.scarcity)
|
|
1271
|
+
return Backup(chosen, effort, native_level(c.platform_id, c.model_id, effort, catalog=catalog))
|
|
1188
1272
|
|
|
1189
1273
|
|
|
1190
1274
|
def _below(lower: Rung, upper: Rung, catalog: dict[str, Any]) -> Rung:
|
|
@@ -1271,14 +1355,19 @@ def render_ladder_table(table: dict[str, Ladder]) -> str:
|
|
|
1271
1355
|
for p in any_ladder.frontier
|
|
1272
1356
|
),
|
|
1273
1357
|
"Rows: <category>/<complexity>[/novel]: COST | BALANCED | QUALITY, each "
|
|
1274
|
-
"<model> @ <platform> · <effort>, the effort in the platform's own level words
|
|
1358
|
+
"<model> @ <platform> · <effort>, the effort in the platform's own level words, "
|
|
1359
|
+
"then `backup <model> @ <platform>`: that pick's BACKUP.",
|
|
1275
1360
|
]
|
|
1361
|
+
|
|
1362
|
+
def cell(t: str, r: Rung) -> str:
|
|
1363
|
+
text = f"{t.upper()} = {r.candidate.model_name} @ {r.candidate.platform_name} · {r.level}"
|
|
1364
|
+
if r.backup is not None:
|
|
1365
|
+
b = r.backup.candidate
|
|
1366
|
+
text += f", backup {b.model_name} @ {b.platform_name}"
|
|
1367
|
+
return text
|
|
1368
|
+
|
|
1276
1369
|
for key, lad in table.items():
|
|
1277
|
-
cells = " | ".join(
|
|
1278
|
-
f"{t.upper()} = {r.candidate.model_name} @ {r.candidate.platform_name} · {r.level}"
|
|
1279
|
-
for t in LADDER_TIERS
|
|
1280
|
-
for r in [lad.rungs[t]]
|
|
1281
|
-
)
|
|
1370
|
+
cells = " | ".join(cell(t, lad.rungs[t]) for t in LADDER_TIERS)
|
|
1282
1371
|
lines.append(f"{key}: {cells}")
|
|
1283
1372
|
lines.append("</ladder-table>")
|
|
1284
1373
|
return "\n".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|