roadmodel 0.2.3__tar.gz → 0.2.4__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 (32) hide show
  1. {roadmodel-0.2.3 → roadmodel-0.2.4}/PKG-INFO +1 -1
  2. {roadmodel-0.2.3 → roadmodel-0.2.4}/pyproject.toml +1 -1
  3. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/__init__.py +1 -1
  4. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/cost.py +25 -0
  5. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/providers/__init__.py +1 -0
  6. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/providers/anthropic.py +9 -8
  7. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/providers/google.py +7 -0
  8. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/providers/openai.py +8 -6
  9. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/recommend.py +20 -1
  10. {roadmodel-0.2.3 → roadmodel-0.2.4}/.gitignore +0 -0
  11. {roadmodel-0.2.3 → roadmodel-0.2.4}/LICENSE +0 -0
  12. {roadmodel-0.2.3 → roadmodel-0.2.4}/NOTICE +0 -0
  13. {roadmodel-0.2.3 → roadmodel-0.2.4}/README.md +0 -0
  14. {roadmodel-0.2.3 → roadmodel-0.2.4}/docs/catalog.json +0 -0
  15. {roadmodel-0.2.3 → roadmodel-0.2.4}/docs/model-selector.txt +0 -0
  16. {roadmodel-0.2.3 → roadmodel-0.2.4}/docs/model-tier-cost-scale.md +0 -0
  17. {roadmodel-0.2.3 → roadmodel-0.2.4}/docs/templates/phase-roadmap-template.md +0 -0
  18. {roadmodel-0.2.3 → roadmodel-0.2.4}/docs/user-context.example.md +0 -0
  19. {roadmodel-0.2.3 → roadmodel-0.2.4}/hatch_build.py +0 -0
  20. {roadmodel-0.2.3 → roadmodel-0.2.4}/infra/README.md +0 -0
  21. {roadmodel-0.2.3 → roadmodel-0.2.4}/infra/supabase/README.md +0 -0
  22. {roadmodel-0.2.3 → roadmodel-0.2.4}/roadmodel/data/catalog.json +0 -0
  23. {roadmodel-0.2.3 → roadmodel-0.2.4}/roadmodel/data/model-selector.txt +0 -0
  24. {roadmodel-0.2.3 → roadmodel-0.2.4}/roadmodel/data/model-tier-cost-scale.md +0 -0
  25. {roadmodel-0.2.3 → roadmodel-0.2.4}/roadmodel/data/phase-roadmap-template.md +0 -0
  26. {roadmodel-0.2.3 → roadmodel-0.2.4}/roadmodel/data/user-context.example.md +0 -0
  27. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/__main__.py +0 -0
  28. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/cli.py +0 -0
  29. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/config.py +0 -0
  30. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/errors.py +0 -0
  31. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/mcp_server.py +0 -0
  32. {roadmodel-0.2.3 → roadmodel-0.2.4}/src/roadmodel/user_context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roadmodel
3
- Version: 0.2.3
3
+ Version: 0.2.4
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "roadmodel"
7
- version = "0.2.3"
7
+ version = "0.2.4"
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,2 +1,2 @@
1
1
  # src/roadmodel/__init__.py
2
- __version__ = "0.2.3"
2
+ __version__ = "0.2.4"
@@ -258,6 +258,31 @@ def _resolve_method(platform_id: str, catalog: dict[str, Any]) -> dict[str, Any]
258
258
  )
259
259
 
260
260
 
261
+ def canonical_model_name(model_ref: str) -> str:
262
+ """Resolve a model id-or-name to its catalog display ``name``; return the
263
+ input unchanged on any catalog miss (never raises).
264
+
265
+ The recommender LLM emits the model freely as either the catalog id/slug
266
+ or the display name, which made the response header (raw) disagree with the
267
+ cost/comparison table (catalog name) and risked silently dropping the cost
268
+ panel on an unrecognized label (#174). Callers canonicalize once so every
269
+ downstream consumer references one consistent name.
270
+ """
271
+ try:
272
+ return str(_resolve_model(model_ref, _load_catalog())["name"])
273
+ except (ValueError, BundledDocNotFoundError):
274
+ return model_ref
275
+
276
+
277
+ def canonical_platform_name(platform_ref: str) -> str:
278
+ """Resolve an access-method id-or-name to its catalog display ``name``;
279
+ return the input unchanged on any catalog miss (never raises) (#174)."""
280
+ try:
281
+ return str(_resolve_method(platform_ref, _load_catalog())["name"])
282
+ except (ValueError, BundledDocNotFoundError):
283
+ return platform_ref
284
+
285
+
261
286
  def _as_dict(value: object) -> dict[str, Any]:
262
287
  if not isinstance(value, dict):
263
288
  raise BundledDocNotFoundError("catalog.json")
@@ -14,4 +14,5 @@ class ProviderAdapter(Protocol):
14
14
  api_key: str,
15
15
  max_output_tokens: int | None = None,
16
16
  thinking_budget: int | None = None,
17
+ temperature: float | None = None,
17
18
  ) -> str: ...
@@ -14,15 +14,16 @@ def recommend(
14
14
  api_key: str,
15
15
  max_output_tokens: int | None = None,
16
16
  thinking_budget: int | None = None,
17
+ temperature: float | None = None,
17
18
  ) -> str:
18
- # thinking_budget is accepted for ProviderAdapter Protocol parity but
19
- # intentionally NOT forwarded: it is a Gemini-specific knob for the
20
- # recommender latency work (issue #132). Anthropic extended-thinking has
21
- # different semantics (a `thinking` block with its own budget_tokens and
22
- # minimums) and the recommender response shape does not tolerate small
23
- # caps on Anthropic at all (PR #128). Anthropic reasoning control is
24
- # Phase 5 paid-frontier scope.
25
- _ = thinking_budget
19
+ # thinking_budget and temperature are accepted for ProviderAdapter Protocol
20
+ # parity but intentionally NOT forwarded: both are Gemini-specific knobs for
21
+ # the recommender latency/determinism work (issues #132, #176). Anthropic
22
+ # extended-thinking has different semantics (a `thinking` block with its own
23
+ # budget_tokens and minimums) and the recommender response shape does not
24
+ # tolerate small caps on Anthropic at all (PR #128). Anthropic reasoning
25
+ # control is Phase 5 paid-frontier scope.
26
+ _ = (thinking_budget, temperature)
26
27
  try:
27
28
  from anthropic import Anthropic, APIError
28
29
  except Exception as exc: # pragma: no cover - dependency/runtime guard
@@ -16,6 +16,7 @@ def recommend(
16
16
  api_key: str,
17
17
  max_output_tokens: int | None = None,
18
18
  thinking_budget: int | None = None,
19
+ temperature: float | None = None,
19
20
  ) -> str:
20
21
  try:
21
22
  from google import genai
@@ -40,6 +41,12 @@ def recommend(
40
41
  # thinking entirely, a small value bounds it. `is not None` —
41
42
  # not truthiness — because 0 is a meaningful value (thinking off).
42
43
  config["thinking_config"] = {"thinking_budget": thinking_budget}
44
+ if temperature is not None:
45
+ # Recommender determinism (#176): without this Gemini samples at
46
+ # its default temperature (~1.0), so identical input yields
47
+ # different model picks run-to-run. `is not None` — not truthiness
48
+ # — because 0.0 (greedy/deterministic) is the intended value.
49
+ config["temperature"] = temperature
43
50
  response = client.models.generate_content(
44
51
  model=model or DEFAULT_MODEL,
45
52
  contents=prompt,
@@ -32,13 +32,15 @@ def recommend(
32
32
  api_key: str,
33
33
  max_output_tokens: int | None = None,
34
34
  thinking_budget: int | None = None,
35
+ temperature: float | None = None,
35
36
  ) -> str:
36
- # thinking_budget is accepted for ProviderAdapter Protocol parity but
37
- # intentionally NOT forwarded: it is a Gemini-specific knob for the
38
- # recommender latency work (issue #132). OpenAI reasoning control uses a
39
- # different mechanism (`reasoning.effort` on reasoning models), out of
40
- # scope for the free-tier recommender, which runs on Gemini Flash.
41
- _ = thinking_budget
37
+ # thinking_budget and temperature are accepted for ProviderAdapter Protocol
38
+ # parity but intentionally NOT forwarded: both are Gemini-specific knobs for
39
+ # the recommender latency/determinism work (issues #132, #176). OpenAI
40
+ # reasoning control uses a different mechanism (`reasoning.effort` on
41
+ # reasoning models), out of scope for the free-tier recommender, which runs
42
+ # on Gemini Flash.
43
+ _ = (thinking_budget, temperature)
42
44
  try:
43
45
  from openai import APIError, OpenAI
44
46
  except Exception as exc: # pragma: no cover - dependency/runtime guard
@@ -125,6 +125,7 @@ def recommend(
125
125
  *,
126
126
  max_output_tokens: int | None = None,
127
127
  thinking_budget: int | None = None,
128
+ temperature: float | None = None,
128
129
  ) -> dict[str, str]:
129
130
  user_context_text = user_context.read(config.user_context_path)
130
131
  system_prompt, user_prompt = build_prompt(prompt, user_context_text=user_context_text)
@@ -136,6 +137,7 @@ def recommend(
136
137
  api_key=config.api_key,
137
138
  max_output_tokens=max_output_tokens,
138
139
  thinking_budget=thinking_budget,
140
+ temperature=temperature,
139
141
  )
140
142
  return parse_response(raw_response)
141
143
 
@@ -172,11 +174,28 @@ def recommend_structured(
172
174
  max_mode: bool = False,
173
175
  max_output_tokens: int | None = None,
174
176
  thinking_budget: int | None = None,
177
+ temperature: float | None = None,
175
178
  ) -> dict[str, Any]:
176
179
  """Return roadmap-style structured output plus optional cost estimates."""
177
180
  base = recommend(
178
- prompt, config, max_output_tokens=max_output_tokens, thinking_budget=thinking_budget
181
+ prompt,
182
+ config,
183
+ max_output_tokens=max_output_tokens,
184
+ thinking_budget=thinking_budget,
185
+ temperature=temperature,
179
186
  )
187
+ # Canonicalize the model + platform to their catalog display names (#174):
188
+ # the LLM emits either the id/slug or the display name freely, which made
189
+ # the response header (raw) disagree with the cost/comparison table
190
+ # (catalog name) and risked a silent cost-panel drop on an unrecognized
191
+ # label. Resolve once here so the payload, per-surface settings, and the
192
+ # cost calls below all agree; falls back to the raw value on a catalog
193
+ # miss (canonical_* never raises).
194
+ base = {
195
+ **base,
196
+ "model": cost.canonical_model_name(base["model"]),
197
+ "platform": cost.canonical_platform_name(base["platform"]),
198
+ }
180
199
  payload: dict[str, Any] = {
181
200
  "model": base["model"],
182
201
  "platform": base["platform"],
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes