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.
- minima/__init__.py +5 -0
- minima/api/__init__.py +1 -0
- minima/api/auth.py +39 -0
- minima/api/errors.py +40 -0
- minima/api/routers/__init__.py +1 -0
- minima/api/routers/calibration.py +50 -0
- minima/api/routers/feedback.py +279 -0
- minima/api/routers/health.py +50 -0
- minima/api/routers/models.py +42 -0
- minima/api/routers/recommend.py +66 -0
- minima/api/routers/savings.py +55 -0
- minima/api/routers/strategies.py +33 -0
- minima/catalog/__init__.py +1 -0
- minima/catalog/data/capability_priors.json +210 -0
- minima/catalog/data/model_aliases.json +12 -0
- minima/catalog/merge.py +69 -0
- minima/catalog/refresh.py +54 -0
- minima/catalog/sources/__init__.py +1 -0
- minima/catalog/sources/litellm.py +19 -0
- minima/catalog/sources/openrouter.py +25 -0
- minima/catalog/store.py +86 -0
- minima/config.py +288 -0
- minima/deps.py +35 -0
- minima/llm/__init__.py +1 -0
- minima/llm/anthropic.py +106 -0
- minima/llm/base.py +196 -0
- minima/llm/gemini.py +124 -0
- minima/llm/registry.py +54 -0
- minima/logging.py +28 -0
- minima/main.py +109 -0
- minima/memory/__init__.py +1 -0
- minima/memory/adapter.py +572 -0
- minima/memory/keys.py +83 -0
- minima/memory/records.py +190 -0
- minima/memory/threadpool.py +41 -0
- minima/metrics/__init__.py +1 -0
- minima/metrics/calibration.py +415 -0
- minima/metrics/report.py +116 -0
- minima/metrics/savings.py +98 -0
- minima/recommender/__init__.py +1 -0
- minima/recommender/_pg_pool.py +38 -0
- minima/recommender/_redis_client.py +32 -0
- minima/recommender/aggregate.py +157 -0
- minima/recommender/classify.py +165 -0
- minima/recommender/decisionlog.py +505 -0
- minima/recommender/durablerefs.py +312 -0
- minima/recommender/engine.py +997 -0
- minima/recommender/escalation.py +83 -0
- minima/recommender/propensity.py +189 -0
- minima/recommender/recstore.py +368 -0
- minima/recommender/score.py +318 -0
- minima/recommender/types.py +166 -0
- minima/schemas/__init__.py +1 -0
- minima/schemas/common.py +73 -0
- minima/schemas/feedback.py +34 -0
- minima/schemas/models_catalog.py +36 -0
- minima/schemas/recommend.py +104 -0
- minima/schemas/savings.py +39 -0
- minima/schemas/strategies.py +57 -0
- minima/schemas/workflow.py +43 -0
- minima/seeding/__init__.py +1 -0
- minima/seeding/items.py +42 -0
- minima/seeding/llmrouterbench.py +232 -0
- minima/seeding/routerbench.py +141 -0
- minima/seeding/run_seed.py +56 -0
- minima/seeding/synthetic.py +70 -0
- minima/tenancy/__init__.py +8 -0
- minima/tenancy/context.py +37 -0
- minima/tenancy/passthrough.py +110 -0
- minima/version.py +3 -0
- minima_cli-0.4.9.dist-info/METADATA +275 -0
- minima_cli-0.4.9.dist-info/RECORD +161 -0
- minima_cli-0.4.9.dist-info/WHEEL +4 -0
- minima_cli-0.4.9.dist-info/entry_points.txt +5 -0
- minima_cli-0.4.9.dist-info/licenses/LICENSE +295 -0
- minima_client/__init__.py +19 -0
- minima_client/autocapture.py +101 -0
- minima_client/client.py +301 -0
- minima_client/errors.py +23 -0
- minima_harness/LICENSE_PI +32 -0
- minima_harness/__init__.py +16 -0
- minima_harness/agent/__init__.py +72 -0
- minima_harness/agent/agent.py +276 -0
- minima_harness/agent/events.py +124 -0
- minima_harness/agent/loop.py +311 -0
- minima_harness/agent/state.py +79 -0
- minima_harness/agent/tools.py +97 -0
- minima_harness/ai/__init__.py +66 -0
- minima_harness/ai/compat.py +71 -0
- minima_harness/ai/errors.py +96 -0
- minima_harness/ai/events.py +117 -0
- minima_harness/ai/openrouter_catalog.py +153 -0
- minima_harness/ai/provider_catalog.py +299 -0
- minima_harness/ai/provider_quirks.py +37 -0
- minima_harness/ai/providers/__init__.py +75 -0
- minima_harness/ai/providers/_common.py +48 -0
- minima_harness/ai/providers/anthropic.py +290 -0
- minima_harness/ai/providers/base.py +65 -0
- minima_harness/ai/providers/faux.py +173 -0
- minima_harness/ai/providers/google.py +221 -0
- minima_harness/ai/providers/openai_compat.py +278 -0
- minima_harness/ai/registry.py +184 -0
- minima_harness/ai/stream.py +82 -0
- minima_harness/ai/tools.py +51 -0
- minima_harness/ai/types.py +204 -0
- minima_harness/ai/usage.py +41 -0
- minima_harness/minima/__init__.py +40 -0
- minima_harness/minima/cache.py +102 -0
- minima_harness/minima/config.py +85 -0
- minima_harness/minima/goals.py +226 -0
- minima_harness/minima/judge.py +144 -0
- minima_harness/minima/mapping.py +147 -0
- minima_harness/minima/meter.py +143 -0
- minima_harness/minima/router.py +220 -0
- minima_harness/minima/runtime.py +544 -0
- minima_harness/minima/signals.py +195 -0
- minima_harness/session/__init__.py +14 -0
- minima_harness/session/format.py +35 -0
- minima_harness/session/store.py +236 -0
- minima_harness/tasks/__init__.py +17 -0
- minima_harness/tasks/task_set.py +78 -0
- minima_harness/tools/__init__.py +7 -0
- minima_harness/tools/_io.py +34 -0
- minima_harness/tools/bash.py +70 -0
- minima_harness/tools/builtin.py +23 -0
- minima_harness/tools/edit.py +50 -0
- minima_harness/tools/find.py +38 -0
- minima_harness/tools/grep.py +73 -0
- minima_harness/tools/ls.py +35 -0
- minima_harness/tools/read.py +38 -0
- minima_harness/tools/tasks.py +75 -0
- minima_harness/tools/write.py +36 -0
- minima_harness/tui/__init__.py +3 -0
- minima_harness/tui/analytics.py +111 -0
- minima_harness/tui/app.py +1927 -0
- minima_harness/tui/bridge.py +103 -0
- minima_harness/tui/cli.py +227 -0
- minima_harness/tui/clipboard.py +60 -0
- minima_harness/tui/commands.py +49 -0
- minima_harness/tui/compaction.py +17 -0
- minima_harness/tui/config_cli.py +141 -0
- minima_harness/tui/config_store.py +237 -0
- minima_harness/tui/context.py +93 -0
- minima_harness/tui/customize.py +95 -0
- minima_harness/tui/diff.py +53 -0
- minima_harness/tui/editor.py +43 -0
- minima_harness/tui/extensions.py +84 -0
- minima_harness/tui/extra_models.py +52 -0
- minima_harness/tui/history.py +71 -0
- minima_harness/tui/mubit.py +295 -0
- minima_harness/tui/overlays.py +593 -0
- minima_harness/tui/packages.py +59 -0
- minima_harness/tui/run_modes.py +66 -0
- minima_harness/tui/theme.py +77 -0
- minima_harness/tui/welcome.py +83 -0
- minima_harness/tui/widgets/__init__.py +3 -0
- minima_harness/tui/widgets/banner.py +38 -0
- minima_harness/tui/widgets/editor.py +83 -0
- minima_harness/tui/widgets/footer.py +73 -0
- minima_harness/tui/widgets/messages.py +151 -0
- 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
|
+
)
|