minima-cli 0.4.9__py3-none-any.whl

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 (161) hide show
  1. minima/__init__.py +5 -0
  2. minima/api/__init__.py +1 -0
  3. minima/api/auth.py +39 -0
  4. minima/api/errors.py +40 -0
  5. minima/api/routers/__init__.py +1 -0
  6. minima/api/routers/calibration.py +50 -0
  7. minima/api/routers/feedback.py +279 -0
  8. minima/api/routers/health.py +50 -0
  9. minima/api/routers/models.py +42 -0
  10. minima/api/routers/recommend.py +66 -0
  11. minima/api/routers/savings.py +55 -0
  12. minima/api/routers/strategies.py +33 -0
  13. minima/catalog/__init__.py +1 -0
  14. minima/catalog/data/capability_priors.json +210 -0
  15. minima/catalog/data/model_aliases.json +12 -0
  16. minima/catalog/merge.py +69 -0
  17. minima/catalog/refresh.py +54 -0
  18. minima/catalog/sources/__init__.py +1 -0
  19. minima/catalog/sources/litellm.py +19 -0
  20. minima/catalog/sources/openrouter.py +25 -0
  21. minima/catalog/store.py +86 -0
  22. minima/config.py +288 -0
  23. minima/deps.py +35 -0
  24. minima/llm/__init__.py +1 -0
  25. minima/llm/anthropic.py +106 -0
  26. minima/llm/base.py +196 -0
  27. minima/llm/gemini.py +124 -0
  28. minima/llm/registry.py +54 -0
  29. minima/logging.py +28 -0
  30. minima/main.py +109 -0
  31. minima/memory/__init__.py +1 -0
  32. minima/memory/adapter.py +572 -0
  33. minima/memory/keys.py +83 -0
  34. minima/memory/records.py +190 -0
  35. minima/memory/threadpool.py +41 -0
  36. minima/metrics/__init__.py +1 -0
  37. minima/metrics/calibration.py +415 -0
  38. minima/metrics/report.py +116 -0
  39. minima/metrics/savings.py +98 -0
  40. minima/recommender/__init__.py +1 -0
  41. minima/recommender/_pg_pool.py +38 -0
  42. minima/recommender/_redis_client.py +32 -0
  43. minima/recommender/aggregate.py +157 -0
  44. minima/recommender/classify.py +165 -0
  45. minima/recommender/decisionlog.py +505 -0
  46. minima/recommender/durablerefs.py +312 -0
  47. minima/recommender/engine.py +997 -0
  48. minima/recommender/escalation.py +83 -0
  49. minima/recommender/propensity.py +189 -0
  50. minima/recommender/recstore.py +368 -0
  51. minima/recommender/score.py +318 -0
  52. minima/recommender/types.py +166 -0
  53. minima/schemas/__init__.py +1 -0
  54. minima/schemas/common.py +73 -0
  55. minima/schemas/feedback.py +34 -0
  56. minima/schemas/models_catalog.py +36 -0
  57. minima/schemas/recommend.py +104 -0
  58. minima/schemas/savings.py +39 -0
  59. minima/schemas/strategies.py +57 -0
  60. minima/schemas/workflow.py +43 -0
  61. minima/seeding/__init__.py +1 -0
  62. minima/seeding/items.py +42 -0
  63. minima/seeding/llmrouterbench.py +232 -0
  64. minima/seeding/routerbench.py +141 -0
  65. minima/seeding/run_seed.py +56 -0
  66. minima/seeding/synthetic.py +70 -0
  67. minima/tenancy/__init__.py +8 -0
  68. minima/tenancy/context.py +37 -0
  69. minima/tenancy/passthrough.py +110 -0
  70. minima/version.py +3 -0
  71. minima_cli-0.4.9.dist-info/METADATA +275 -0
  72. minima_cli-0.4.9.dist-info/RECORD +161 -0
  73. minima_cli-0.4.9.dist-info/WHEEL +4 -0
  74. minima_cli-0.4.9.dist-info/entry_points.txt +5 -0
  75. minima_cli-0.4.9.dist-info/licenses/LICENSE +295 -0
  76. minima_client/__init__.py +19 -0
  77. minima_client/autocapture.py +101 -0
  78. minima_client/client.py +301 -0
  79. minima_client/errors.py +23 -0
  80. minima_harness/LICENSE_PI +32 -0
  81. minima_harness/__init__.py +16 -0
  82. minima_harness/agent/__init__.py +72 -0
  83. minima_harness/agent/agent.py +276 -0
  84. minima_harness/agent/events.py +124 -0
  85. minima_harness/agent/loop.py +311 -0
  86. minima_harness/agent/state.py +79 -0
  87. minima_harness/agent/tools.py +97 -0
  88. minima_harness/ai/__init__.py +66 -0
  89. minima_harness/ai/compat.py +71 -0
  90. minima_harness/ai/errors.py +96 -0
  91. minima_harness/ai/events.py +117 -0
  92. minima_harness/ai/openrouter_catalog.py +153 -0
  93. minima_harness/ai/provider_catalog.py +299 -0
  94. minima_harness/ai/provider_quirks.py +37 -0
  95. minima_harness/ai/providers/__init__.py +75 -0
  96. minima_harness/ai/providers/_common.py +48 -0
  97. minima_harness/ai/providers/anthropic.py +290 -0
  98. minima_harness/ai/providers/base.py +65 -0
  99. minima_harness/ai/providers/faux.py +173 -0
  100. minima_harness/ai/providers/google.py +221 -0
  101. minima_harness/ai/providers/openai_compat.py +278 -0
  102. minima_harness/ai/registry.py +184 -0
  103. minima_harness/ai/stream.py +82 -0
  104. minima_harness/ai/tools.py +51 -0
  105. minima_harness/ai/types.py +204 -0
  106. minima_harness/ai/usage.py +41 -0
  107. minima_harness/minima/__init__.py +40 -0
  108. minima_harness/minima/cache.py +102 -0
  109. minima_harness/minima/config.py +85 -0
  110. minima_harness/minima/goals.py +226 -0
  111. minima_harness/minima/judge.py +144 -0
  112. minima_harness/minima/mapping.py +147 -0
  113. minima_harness/minima/meter.py +143 -0
  114. minima_harness/minima/router.py +220 -0
  115. minima_harness/minima/runtime.py +544 -0
  116. minima_harness/minima/signals.py +195 -0
  117. minima_harness/session/__init__.py +14 -0
  118. minima_harness/session/format.py +35 -0
  119. minima_harness/session/store.py +236 -0
  120. minima_harness/tasks/__init__.py +17 -0
  121. minima_harness/tasks/task_set.py +78 -0
  122. minima_harness/tools/__init__.py +7 -0
  123. minima_harness/tools/_io.py +34 -0
  124. minima_harness/tools/bash.py +70 -0
  125. minima_harness/tools/builtin.py +23 -0
  126. minima_harness/tools/edit.py +50 -0
  127. minima_harness/tools/find.py +38 -0
  128. minima_harness/tools/grep.py +73 -0
  129. minima_harness/tools/ls.py +35 -0
  130. minima_harness/tools/read.py +38 -0
  131. minima_harness/tools/tasks.py +75 -0
  132. minima_harness/tools/write.py +36 -0
  133. minima_harness/tui/__init__.py +3 -0
  134. minima_harness/tui/analytics.py +111 -0
  135. minima_harness/tui/app.py +1927 -0
  136. minima_harness/tui/bridge.py +103 -0
  137. minima_harness/tui/cli.py +227 -0
  138. minima_harness/tui/clipboard.py +60 -0
  139. minima_harness/tui/commands.py +49 -0
  140. minima_harness/tui/compaction.py +17 -0
  141. minima_harness/tui/config_cli.py +141 -0
  142. minima_harness/tui/config_store.py +237 -0
  143. minima_harness/tui/context.py +93 -0
  144. minima_harness/tui/customize.py +95 -0
  145. minima_harness/tui/diff.py +53 -0
  146. minima_harness/tui/editor.py +43 -0
  147. minima_harness/tui/extensions.py +84 -0
  148. minima_harness/tui/extra_models.py +52 -0
  149. minima_harness/tui/history.py +71 -0
  150. minima_harness/tui/mubit.py +295 -0
  151. minima_harness/tui/overlays.py +593 -0
  152. minima_harness/tui/packages.py +59 -0
  153. minima_harness/tui/run_modes.py +66 -0
  154. minima_harness/tui/theme.py +77 -0
  155. minima_harness/tui/welcome.py +83 -0
  156. minima_harness/tui/widgets/__init__.py +3 -0
  157. minima_harness/tui/widgets/banner.py +38 -0
  158. minima_harness/tui/widgets/editor.py +83 -0
  159. minima_harness/tui/widgets/footer.py +73 -0
  160. minima_harness/tui/widgets/messages.py +151 -0
  161. minima_harness/tui/widgets/status.py +57 -0
@@ -0,0 +1,220 @@
1
+ """MinimaRouter — the thin seam between the harness and a running Minima service.
2
+
3
+ Owns the two halves of the Minima loop on the harness side: ``recommend`` (ask Minima
4
+ which model, map it to a callable harness model) and ``feedback`` (report the realized
5
+ tokens / cost / latency / quality so Minima's memory sharpens). Realized cost comes from
6
+ the provider's actual usage (``usage.cost.total``), NOT Minima's prior estimate — that is
7
+ what lets the cost basis climb estimate -> observed -> rescaled.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from dataclasses import dataclass, field
14
+ from urllib.parse import urlsplit
15
+
16
+ from minima_client import AsyncMinimaClient
17
+ from minima_client.errors import MinimaError
18
+
19
+ from minima.schemas.common import Constraints
20
+ from minima_harness.ai.types import Model, Usage
21
+ from minima_harness.minima.config import HarnessConfig
22
+ from minima_harness.minima.mapping import ModelMapping
23
+
24
+ _log = logging.getLogger("minima_harness.router")
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class Ranking:
29
+ """A harness-native view of one ranked candidate (no minima schema leak)."""
30
+
31
+ model_id: str
32
+ provider: str
33
+ predicted_success: float
34
+ est_cost_usd: float
35
+ rationale: str = ""
36
+ decision_basis: str = ""
37
+ # Speed + predictability axes (server provides these; the harness now surfaces them).
38
+ est_latency_ms: float | None = None
39
+ latency_basis: str = ""
40
+ est_cost_low: float | None = None
41
+ est_cost_high: float | None = None
42
+ cost_band_basis: str = ""
43
+ success_interval_width: float = 0.0
44
+ evidence_count: int = 0
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class RoutingResult:
49
+ """The outcome of a routing decision for one prompt.
50
+
51
+ Carries Minima's full explainability payload (ranked list, rationale, warnings,
52
+ threshold, confidence, fallback) plus ``baseline_cost_usd`` — the estimated cost of
53
+ ``config.baseline_model_id`` within the ranked set, which powers the cost meter's
54
+ "savings vs your default" number.
55
+ """
56
+
57
+ recommendation_id: str | None
58
+ chosen_model_id: str | None
59
+ model: Model
60
+ est_cost_usd: float
61
+ decision_basis: str
62
+ ranked: list[Ranking] = field(default_factory=list)
63
+ rationale: str = ""
64
+ warnings: list[str] = field(default_factory=list)
65
+ threshold_used: float = 0.0
66
+ confidence: float = 0.0
67
+ fallback_model_id: str | None = None
68
+ baseline_cost_usd: float | None = None
69
+ # Predictable cost band for the chosen model (None when evidence is thin).
70
+ est_cost_low: float | None = None
71
+ est_cost_high: float | None = None
72
+ cost_band_basis: str = ""
73
+
74
+
75
+ def _needs_auth(url: str) -> bool:
76
+ """True for a hosted/remote Minima (always requires a Bearer key). A local server
77
+ (localhost/loopback) may be keyless, so we don't pre-judge a missing key there."""
78
+ host = (urlsplit(url).hostname or "").lower()
79
+ return bool(host) and host not in ("localhost", "127.0.0.1", "::1")
80
+
81
+
82
+ def _baseline_cost(ranked: list[Ranking], baseline_id: str | None) -> float | None:
83
+ if not baseline_id:
84
+ return None
85
+ for r in ranked:
86
+ if r.model_id == baseline_id:
87
+ return r.est_cost_usd
88
+ return None
89
+
90
+
91
+ class MinimaRouter:
92
+ def __init__(
93
+ self,
94
+ client: AsyncMinimaClient,
95
+ config: HarnessConfig,
96
+ mapping: ModelMapping | None = None,
97
+ ) -> None:
98
+ self._client = client
99
+ self.config = config
100
+ self.mapping = mapping or ModelMapping()
101
+
102
+ @classmethod
103
+ def for_config(cls, config: HarnessConfig, mapping: ModelMapping | None = None) -> MinimaRouter:
104
+ client = AsyncMinimaClient(config.minima_url, config.minima_api_key, config.timeout)
105
+ return cls(client, config, mapping)
106
+
107
+ async def aclose(self) -> None:
108
+ """Close the underlying HTTP client (called when a reconnect replaces this router)."""
109
+ try:
110
+ await self._client.aclose()
111
+ except Exception: # noqa: BLE001 - best-effort cleanup; never block a reconnect
112
+ pass
113
+
114
+ async def recommend(
115
+ self,
116
+ task: str,
117
+ *,
118
+ task_type: str | None = None,
119
+ slider: float | None = None,
120
+ tags: list[str] | None = None,
121
+ difficulty: str | None = None,
122
+ expected_input_tokens: int | None = None,
123
+ candidates: list[str] | None = None,
124
+ ) -> RoutingResult:
125
+ # Routing explicitly disabled (e.g. `--offline` sets minima_url=""). Fail fast with a
126
+ # clear reason instead of letting httpx raise UnsupportedProtocol on a scheme-less URL.
127
+ if not (self.config.minima_url or "").strip():
128
+ raise RuntimeError("routing disabled (offline mode)")
129
+ # A hosted Minima always needs a Bearer key; with none configured the request is a
130
+ # guaranteed 401. Skip the doomed round-trip and surface the actionable reason (the
131
+ # client's auth header is fixed at build time, so this also can't be a stale-key race).
132
+ if not (self.config.minima_api_key or "").strip() and _needs_auth(self.config.minima_url):
133
+ raise MinimaError(401, "no Mubit API key configured")
134
+ # Caller may pass an already-narrowed candidate set (e.g. the runtime drops providers whose
135
+ # key is absent or has auth-failed this session); otherwise fall back to the configured set.
136
+ effective = candidates if candidates is not None else list(self.config.candidates)
137
+ constraints = Constraints(candidate_models=list(effective)) if effective else None
138
+ # Build a TaskInput only when code-quality signals (or a task_type) enrich it;
139
+ # otherwise pass the bare prompt string (the cheaper wire shape).
140
+ task_input: dict | str = task
141
+ if task_type or tags or difficulty or expected_input_tokens is not None:
142
+ task_input = {"task": task}
143
+ if task_type:
144
+ task_input["task_type"] = task_type
145
+ if tags:
146
+ task_input["tags"] = tags
147
+ if difficulty:
148
+ task_input["difficulty"] = difficulty
149
+ if expected_input_tokens is not None:
150
+ task_input["expected_input_tokens"] = expected_input_tokens
151
+ rec = await self._client.recommend(
152
+ task_input,
153
+ cost_quality_tradeoff=slider
154
+ if slider is not None
155
+ else self.config.cost_quality_tradeoff,
156
+ constraints=constraints,
157
+ namespace=self.config.namespace,
158
+ baseline_model_id=self.config.baseline_model_id,
159
+ )
160
+ ranked = rec.recommended_model
161
+ model = self.mapping.to_model(ranked, offline_default=self.mapping.default_model())
162
+ ranking_list = [
163
+ Ranking(
164
+ model_id=r.model_id,
165
+ provider=r.provider,
166
+ predicted_success=r.predicted_success,
167
+ est_cost_usd=r.est_cost_usd,
168
+ rationale=r.rationale,
169
+ decision_basis=str(r.decision_basis),
170
+ est_latency_ms=r.est_latency_ms,
171
+ latency_basis=r.latency_basis,
172
+ est_cost_low=r.est_cost_low,
173
+ est_cost_high=r.est_cost_high,
174
+ cost_band_basis=r.cost_band_basis,
175
+ success_interval_width=r.success_interval_width,
176
+ evidence_count=len(r.evidence),
177
+ )
178
+ for r in rec.ranked
179
+ ]
180
+ return RoutingResult(
181
+ recommendation_id=rec.recommendation_id,
182
+ chosen_model_id=ranked.model_id,
183
+ model=model,
184
+ est_cost_usd=ranked.est_cost_usd,
185
+ decision_basis=str(rec.decision_basis),
186
+ ranked=ranking_list,
187
+ rationale=ranked.rationale,
188
+ warnings=list(rec.warnings),
189
+ threshold_used=rec.threshold_used,
190
+ confidence=rec.confidence,
191
+ fallback_model_id=rec.fallback_model.model_id if rec.fallback_model else None,
192
+ baseline_cost_usd=_baseline_cost(ranking_list, self.config.baseline_model_id),
193
+ est_cost_low=ranked.est_cost_low,
194
+ est_cost_high=ranked.est_cost_high,
195
+ cost_band_basis=ranked.cost_band_basis,
196
+ )
197
+
198
+ async def feedback(
199
+ self,
200
+ recommendation_id: str,
201
+ chosen_model_id: str,
202
+ outcome: str,
203
+ *,
204
+ quality: float | None,
205
+ usage: Usage,
206
+ latency_ms: int,
207
+ iterations: int | None = None,
208
+ ) -> None:
209
+ await self._client.feedback(
210
+ recommendation_id,
211
+ chosen_model_id,
212
+ outcome,
213
+ quality_score=quality,
214
+ input_tokens=usage.input or None,
215
+ output_tokens=usage.output or None,
216
+ actual_cost_usd=round(usage.cost.total, 8),
217
+ latency_ms=latency_ms,
218
+ iterations=iterations,
219
+ verified_in_production=True,
220
+ )