roadmodel 0.2.2__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.2 → roadmodel-0.2.4}/PKG-INFO +1 -1
  2. {roadmodel-0.2.2 → roadmodel-0.2.4}/pyproject.toml +1 -1
  3. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/__init__.py +1 -1
  4. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/cost.py +25 -0
  5. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/providers/__init__.py +2 -0
  6. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/providers/anthropic.py +10 -0
  7. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/providers/google.py +15 -0
  8. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/providers/openai.py +9 -0
  9. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/recommend.py +25 -1
  10. {roadmodel-0.2.2 → roadmodel-0.2.4}/.gitignore +0 -0
  11. {roadmodel-0.2.2 → roadmodel-0.2.4}/LICENSE +0 -0
  12. {roadmodel-0.2.2 → roadmodel-0.2.4}/NOTICE +0 -0
  13. {roadmodel-0.2.2 → roadmodel-0.2.4}/README.md +0 -0
  14. {roadmodel-0.2.2 → roadmodel-0.2.4}/docs/catalog.json +0 -0
  15. {roadmodel-0.2.2 → roadmodel-0.2.4}/docs/model-selector.txt +0 -0
  16. {roadmodel-0.2.2 → roadmodel-0.2.4}/docs/model-tier-cost-scale.md +0 -0
  17. {roadmodel-0.2.2 → roadmodel-0.2.4}/docs/templates/phase-roadmap-template.md +0 -0
  18. {roadmodel-0.2.2 → roadmodel-0.2.4}/docs/user-context.example.md +0 -0
  19. {roadmodel-0.2.2 → roadmodel-0.2.4}/hatch_build.py +0 -0
  20. {roadmodel-0.2.2 → roadmodel-0.2.4}/infra/README.md +0 -0
  21. {roadmodel-0.2.2 → roadmodel-0.2.4}/infra/supabase/README.md +0 -0
  22. {roadmodel-0.2.2 → roadmodel-0.2.4}/roadmodel/data/catalog.json +0 -0
  23. {roadmodel-0.2.2 → roadmodel-0.2.4}/roadmodel/data/model-selector.txt +0 -0
  24. {roadmodel-0.2.2 → roadmodel-0.2.4}/roadmodel/data/model-tier-cost-scale.md +0 -0
  25. {roadmodel-0.2.2 → roadmodel-0.2.4}/roadmodel/data/phase-roadmap-template.md +0 -0
  26. {roadmodel-0.2.2 → roadmodel-0.2.4}/roadmodel/data/user-context.example.md +0 -0
  27. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/__main__.py +0 -0
  28. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/cli.py +0 -0
  29. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/config.py +0 -0
  30. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/errors.py +0 -0
  31. {roadmodel-0.2.2 → roadmodel-0.2.4}/src/roadmodel/mcp_server.py +0 -0
  32. {roadmodel-0.2.2 → 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.2
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.2"
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.2"
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")
@@ -13,4 +13,6 @@ class ProviderAdapter(Protocol):
13
13
  model: str | None = None,
14
14
  api_key: str,
15
15
  max_output_tokens: int | None = None,
16
+ thinking_budget: int | None = None,
17
+ temperature: float | None = None,
16
18
  ) -> str: ...
@@ -13,7 +13,17 @@ def recommend(
13
13
  model: str | None = None,
14
14
  api_key: str,
15
15
  max_output_tokens: int | None = None,
16
+ thinking_budget: int | None = None,
17
+ temperature: float | None = None,
16
18
  ) -> str:
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)
17
27
  try:
18
28
  from anthropic import Anthropic, APIError
19
29
  except Exception as exc: # pragma: no cover - dependency/runtime guard
@@ -15,6 +15,8 @@ def recommend(
15
15
  model: str | None = None,
16
16
  api_key: str,
17
17
  max_output_tokens: int | None = None,
18
+ thinking_budget: int | None = None,
19
+ temperature: float | None = None,
18
20
  ) -> str:
19
21
  try:
20
22
  from google import genai
@@ -32,6 +34,19 @@ def recommend(
32
34
  config: Any = {"system_instruction": system}
33
35
  if max_output_tokens is not None:
34
36
  config["max_output_tokens"] = max_output_tokens
37
+ if thinking_budget is not None:
38
+ # Gemini 2.5+ Flash reasons by default, and that reasoning is
39
+ # decoded before the visible answer (and counts against
40
+ # max_output_tokens). thinking_budget caps it: 0 disables
41
+ # thinking entirely, a small value bounds it. `is not None` —
42
+ # not truthiness — because 0 is a meaningful value (thinking off).
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
35
50
  response = client.models.generate_content(
36
51
  model=model or DEFAULT_MODEL,
37
52
  contents=prompt,
@@ -31,7 +31,16 @@ def recommend(
31
31
  model: str | None = None,
32
32
  api_key: str,
33
33
  max_output_tokens: int | None = None,
34
+ thinking_budget: int | None = None,
35
+ temperature: float | None = None,
34
36
  ) -> str:
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)
35
44
  try:
36
45
  from openai import APIError, OpenAI
37
46
  except Exception as exc: # pragma: no cover - dependency/runtime guard
@@ -124,6 +124,8 @@ def recommend(
124
124
  config: Config,
125
125
  *,
126
126
  max_output_tokens: int | None = None,
127
+ thinking_budget: int | None = None,
128
+ temperature: float | None = None,
127
129
  ) -> dict[str, str]:
128
130
  user_context_text = user_context.read(config.user_context_path)
129
131
  system_prompt, user_prompt = build_prompt(prompt, user_context_text=user_context_text)
@@ -134,6 +136,8 @@ def recommend(
134
136
  model=config.model,
135
137
  api_key=config.api_key,
136
138
  max_output_tokens=max_output_tokens,
139
+ thinking_budget=thinking_budget,
140
+ temperature=temperature,
137
141
  )
138
142
  return parse_response(raw_response)
139
143
 
@@ -169,9 +173,29 @@ def recommend_structured(
169
173
  output_tokens: int | None = None,
170
174
  max_mode: bool = False,
171
175
  max_output_tokens: int | None = None,
176
+ thinking_budget: int | None = None,
177
+ temperature: float | None = None,
172
178
  ) -> dict[str, Any]:
173
179
  """Return roadmap-style structured output plus optional cost estimates."""
174
- base = recommend(prompt, config, max_output_tokens=max_output_tokens)
180
+ base = recommend(
181
+ prompt,
182
+ config,
183
+ max_output_tokens=max_output_tokens,
184
+ thinking_budget=thinking_budget,
185
+ temperature=temperature,
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
+ }
175
199
  payload: dict[str, Any] = {
176
200
  "model": base["model"],
177
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