better-rlm 0.3.0__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 (64) hide show
  1. better_rlm/__init__.py +1 -0
  2. better_rlm/auth.py +204 -0
  3. better_rlm/batch.py +550 -0
  4. better_rlm/budget.py +733 -0
  5. better_rlm/chunking.py +193 -0
  6. better_rlm/cli.py +186 -0
  7. better_rlm/config.py +413 -0
  8. better_rlm/config_writer.py +245 -0
  9. better_rlm/context_store.py +615 -0
  10. better_rlm/deps.py +97 -0
  11. better_rlm/describe.py +148 -0
  12. better_rlm/engine.py +335 -0
  13. better_rlm/logsetup.py +333 -0
  14. better_rlm/models.py +71 -0
  15. better_rlm/output.py +110 -0
  16. better_rlm/ratelimit.py +182 -0
  17. better_rlm/results.py +303 -0
  18. better_rlm/sandbox_reap.py +155 -0
  19. better_rlm/server.py +715 -0
  20. better_rlm/shutdown.py +71 -0
  21. better_rlm/sources.py +249 -0
  22. better_rlm/subquery.py +138 -0
  23. better_rlm/transport.py +640 -0
  24. better_rlm/tui.py +621 -0
  25. better_rlm/version.py +42 -0
  26. better_rlm-0.3.0.dist-info/METADATA +1131 -0
  27. better_rlm-0.3.0.dist-info/RECORD +64 -0
  28. better_rlm-0.3.0.dist-info/WHEEL +5 -0
  29. better_rlm-0.3.0.dist-info/entry_points.txt +2 -0
  30. better_rlm-0.3.0.dist-info/licenses/LICENSE +22 -0
  31. better_rlm-0.3.0.dist-info/licenses/NOTICE +17 -0
  32. better_rlm-0.3.0.dist-info/top_level.txt +2 -0
  33. rlm/__init__.py +17 -0
  34. rlm/clients/__init__.py +59 -0
  35. rlm/clients/anthropic.py +112 -0
  36. rlm/clients/azure_openai.py +144 -0
  37. rlm/clients/base_lm.py +47 -0
  38. rlm/clients/gemini.py +164 -0
  39. rlm/clients/openai.py +203 -0
  40. rlm/clients/portkey.py +94 -0
  41. rlm/core/__init__.py +0 -0
  42. rlm/core/comms_utils.py +267 -0
  43. rlm/core/lm_handler.py +266 -0
  44. rlm/core/rlm.py +1040 -0
  45. rlm/core/types.py +309 -0
  46. rlm/environments/__init__.py +82 -0
  47. rlm/environments/base_env.py +388 -0
  48. rlm/environments/constants.py +32 -0
  49. rlm/environments/daytona_repl.py +705 -0
  50. rlm/environments/docker_repl.py +933 -0
  51. rlm/environments/e2b_repl.py +512 -0
  52. rlm/environments/ipython_repl.py +1519 -0
  53. rlm/environments/local_repl.py +612 -0
  54. rlm/environments/modal_repl.py +515 -0
  55. rlm/environments/prime_repl.py +601 -0
  56. rlm/logger/__init__.py +4 -0
  57. rlm/logger/rlm_logger.py +91 -0
  58. rlm/logger/verbose.py +538 -0
  59. rlm/utils/__init__.py +0 -0
  60. rlm/utils/exceptions.py +125 -0
  61. rlm/utils/parsing.py +134 -0
  62. rlm/utils/prompts.py +285 -0
  63. rlm/utils/rlm_utils.py +12 -0
  64. rlm/utils/token_utils.py +154 -0
better_rlm/__init__.py ADDED
@@ -0,0 +1 @@
1
+
better_rlm/auth.py ADDED
@@ -0,0 +1,204 @@
1
+ """Authentication & transport-mode selection — reuse Claude Code's login, no API key.
2
+
3
+ A transport **Strategy** (better_rlm/transport.py) decides how each model call is made;
4
+ this module decides *which* transport, from ``cfg.mode`` (``auto`` | ``claude-cli``
5
+ | ``api``, overridable via the ``RLM_MODE`` env var):
6
+
7
+ * ``claude-cli`` → drive the official ``claude`` CLI (``claude -p``). The CLI is
8
+ itself the authorized Claude Code tool, so it authenticates from your existing
9
+ login (keychain, or a ``claude setup-token`` token if present). **No API key and
10
+ no token plumbing of our own** — and no premium-model gating to work around.
11
+ * ``api`` → the Anthropic SDK, using ``ANTHROPIC_API_KEY``.
12
+ * ``auto`` (default) → prefer the ``claude`` CLI when it's installed (zero setup —
13
+ reuse the Claude Code login); otherwise fall back to ``ANTHROPIC_API_KEY``.
14
+
15
+ ``resolve_auth_mode(cfg)`` returns the internal selector ``"oauth"`` (claude CLI) or
16
+ ``"apikey"`` (SDK) consumed by the transport, model-selection strategy, and retry
17
+ backoff. The engine's AnthropicClient hardcodes api_key + its own retries, so
18
+ ``patch_engine()`` rebinds it to a subclass that routes every completion through the
19
+ chosen transport + the shared throttle/retry (ratelimit.py).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import shutil
26
+
27
+ import anthropic
28
+
29
+ # Importing .config runs its module body, which loads .env — no second load here.
30
+ from .config import Config
31
+
32
+ MODE_AUTO = "auto"
33
+ MODE_CLI = "claude-cli"
34
+ MODE_API = "api"
35
+
36
+ _CLIENT_TIMEOUT = 600.0
37
+ _SDK_MAX_RETRIES = 0 # retry/backoff is owned by ratelimit.retry_and_queue_retries
38
+
39
+
40
+ def _clean_secret(value: str | None) -> str | None:
41
+ if not value:
42
+ return None
43
+ v = value.strip()
44
+ if not v or v.startswith("<") or v.endswith(">") or any(c.isspace() for c in v):
45
+ return None
46
+ return v
47
+
48
+
49
+ def require_anthropic(cfg: Config) -> None:
50
+ """Refuse any provider but Anthropic, at every door that leads to a model call.
51
+
52
+ Not a policy preference — a correctness guard. The session-window ledger, the 95%
53
+ floor and the ceiling-learning all live in ``transport._LedgeredTransport``, which is
54
+ only ever in the stack when the Anthropic client has been rebound onto
55
+ ``get_transport``. The other providers kept the engine's own client, which resolves no
56
+ transport, so a Gemini/OpenAI run recorded no spend, passed no gate and learned no
57
+ ceiling: the budget existed on paper and refused nothing. Half-gated spending is worse
58
+ than none, because the estimate then reports headroom that was already consumed.
59
+
60
+ Raised here rather than in ``load_config`` on purpose: the read-only tools (grep,
61
+ exec, chunk, inspect) never call a model and must keep working whatever the config
62
+ says. Only the paths that are about to spend refuse.
63
+ """
64
+ provider = (cfg.provider or "anthropic").strip().lower()
65
+ if provider != "anthropic":
66
+ raise NotImplementedError(
67
+ f"provider={provider!r} is not supported: only provider=anthropic routes "
68
+ "through the session-window budget (ledger, 95% floor, resumable stop). "
69
+ "Set provider: anthropic in config.yaml."
70
+ )
71
+
72
+
73
+ def claude_cli_available(cfg: Config) -> bool:
74
+ """True if the ``claude`` CLI binary is on PATH. Login is the CLI's own concern —
75
+ a present-but-not-logged-in CLI surfaces a clear auth error at call time."""
76
+ return shutil.which(cfg.cli_path) is not None
77
+
78
+
79
+ def auth_status() -> str:
80
+ """Which explicit credential is present in the environment (for status display):
81
+ 'oauth' (a CLAUDE_CODE_OAUTH_TOKEN), 'apikey' (ANTHROPIC_API_KEY), or 'none'.
82
+ NOTE: claude-cli mode needs NEITHER — the CLI uses its own keychain login."""
83
+ if _clean_secret(os.getenv("CLAUDE_CODE_OAUTH_TOKEN")):
84
+ return "oauth"
85
+ if _clean_secret(os.getenv("ANTHROPIC_API_KEY")):
86
+ return "apikey"
87
+ return "none"
88
+
89
+
90
+ def resolve_auth_mode(cfg: Config) -> str:
91
+ """Authoritative transport selector → 'oauth' (claude CLI) | 'apikey' (SDK).
92
+
93
+ Honors ``cfg.mode`` — the three values config.yaml documents. ``auto`` prefers
94
+ the ``claude`` CLI — reusing the existing Claude Code login, so NO token or API
95
+ key is needed — and falls back to ``ANTHROPIC_API_KEY``.
96
+ """
97
+ # Anthropic is the only supported provider, and the only one with a keyless path
98
+ # (the `claude` CLI login). Everything below chooses between ITS two transports.
99
+ require_anthropic(cfg)
100
+ m = (cfg.mode or MODE_AUTO).strip().lower()
101
+ if m == MODE_CLI:
102
+ if not claude_cli_available(cfg):
103
+ raise RuntimeError(
104
+ f"mode=claude-cli but the `claude` CLI was not found (cli_path={cfg.cli_path!r}). "
105
+ "Install Claude Code and log in, or set mode=api with ANTHROPIC_API_KEY."
106
+ )
107
+ return "oauth"
108
+ if m == MODE_API:
109
+ if not _clean_secret(os.getenv("ANTHROPIC_API_KEY")):
110
+ raise RuntimeError("mode=api but ANTHROPIC_API_KEY is not set.")
111
+ return "apikey"
112
+ # auto: prefer the CLI (reuse the Claude Code login), else the API key.
113
+ if claude_cli_available(cfg):
114
+ return "oauth"
115
+ if _clean_secret(os.getenv("ANTHROPIC_API_KEY")):
116
+ return "apikey"
117
+ raise RuntimeError(
118
+ "No transport available. Either install + log into the `claude` CLI "
119
+ "(recommended — reuses your Claude Code login, no key needed), or set "
120
+ "ANTHROPIC_API_KEY and mode=api."
121
+ )
122
+
123
+
124
+ def make_client(async_: bool = False):
125
+ """Build an Anthropic SDK client for the **api** transport, from ANTHROPIC_API_KEY
126
+ (used by transport.ApiTransport). The claude-cli transport does NOT use this — the
127
+ CLI authenticates itself. SDK retries are disabled (ratelimit.py owns retry)."""
128
+ key = _clean_secret(os.getenv("ANTHROPIC_API_KEY"))
129
+ if not key:
130
+ raise RuntimeError(
131
+ "The API transport requires ANTHROPIC_API_KEY. Use mode=claude-cli "
132
+ "(or the default 'auto') to reuse your Claude Code login instead."
133
+ )
134
+ cls = anthropic.AsyncAnthropic if async_ else anthropic.Anthropic
135
+ return cls(api_key=key, timeout=_CLIENT_TIMEOUT, max_retries=_SDK_MAX_RETRIES)
136
+
137
+
138
+ def patch_engine() -> None:
139
+ """Make the engine's model calls go through our transport + throttle/retry.
140
+
141
+ Rebind ``AnthropicClient`` so every completion routes through the selected transport —
142
+ claude CLI on 'oauth', Anthropic SDK on 'apikey' — and therefore through the ledger,
143
+ the floor and the retry. Idempotent.
144
+
145
+ Any other provider raises: see ``require_anthropic``. It used to fall back to the
146
+ engine's native client with only a throttle bolted on, which quietly bypassed the
147
+ whole budget.
148
+ """
149
+ from .config import load_config
150
+
151
+ cfg = load_config()
152
+ require_anthropic(cfg)
153
+
154
+ import rlm.clients.anthropic as ant_mod
155
+
156
+ if getattr(ant_mod, "_rlmmcp_patched", False):
157
+ return
158
+ from .ratelimit import aretry_and_queue_retries, retry_and_queue_retries
159
+ from .transport import get_transport, split_prompt
160
+
161
+ base = ant_mod.AnthropicClient
162
+
163
+ class _ClaudeCodeAnthropicClient(base): # type: ignore[misc, valid-type]
164
+ def __init__(self, api_key=None, model_name=None, max_tokens=32768, **kwargs):
165
+ # The placeholder key satisfies the engine's required field; the real
166
+ # auth lives in the transport (CLI session or SDK api key).
167
+ super().__init__(api_key="rlm-mcp-oauth", model_name=model_name,
168
+ max_tokens=max_tokens, **kwargs)
169
+ # Strategy: pick transport once, by the resolved mode.
170
+ self._transport = get_transport(resolve_auth_mode(cfg), cfg)
171
+
172
+ def _resolve(self, prompt, model):
173
+ messages, system = split_prompt(prompt)
174
+ model = model or self.model_name
175
+ if not model:
176
+ raise ValueError("Model name is required for the Anthropic client.")
177
+ return messages, system, model
178
+
179
+ def _record(self, model, input_tokens, output_tokens):
180
+ # Mirror the engine's _track_cost so get_last_usage()/get_usage_summary()
181
+ # report correct per-model token counts (this proves routing).
182
+ self.model_call_counts[model] += 1
183
+ self.model_input_tokens[model] += input_tokens
184
+ self.model_output_tokens[model] += output_tokens
185
+ self.model_total_tokens[model] += input_tokens + output_tokens
186
+ self.last_prompt_tokens = input_tokens
187
+ self.last_completion_tokens = output_tokens
188
+
189
+ @retry_and_queue_retries
190
+ def completion(self, prompt, model=None):
191
+ messages, system, model = self._resolve(prompt, model)
192
+ res = self._transport.complete(messages, system, model, self.max_tokens)
193
+ self._record(res.model, res.input_tokens, res.output_tokens)
194
+ return res.text
195
+
196
+ @aretry_and_queue_retries
197
+ async def acompletion(self, prompt, model=None):
198
+ messages, system, model = self._resolve(prompt, model)
199
+ res = await self._transport.acomplete(messages, system, model, self.max_tokens)
200
+ self._record(res.model, res.input_tokens, res.output_tokens)
201
+ return res.text
202
+
203
+ ant_mod.AnthropicClient = _ClaudeCodeAnthropicClient
204
+ ant_mod._rlmmcp_patched = True