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.
- better_rlm/__init__.py +1 -0
- better_rlm/auth.py +204 -0
- better_rlm/batch.py +550 -0
- better_rlm/budget.py +733 -0
- better_rlm/chunking.py +193 -0
- better_rlm/cli.py +186 -0
- better_rlm/config.py +413 -0
- better_rlm/config_writer.py +245 -0
- better_rlm/context_store.py +615 -0
- better_rlm/deps.py +97 -0
- better_rlm/describe.py +148 -0
- better_rlm/engine.py +335 -0
- better_rlm/logsetup.py +333 -0
- better_rlm/models.py +71 -0
- better_rlm/output.py +110 -0
- better_rlm/ratelimit.py +182 -0
- better_rlm/results.py +303 -0
- better_rlm/sandbox_reap.py +155 -0
- better_rlm/server.py +715 -0
- better_rlm/shutdown.py +71 -0
- better_rlm/sources.py +249 -0
- better_rlm/subquery.py +138 -0
- better_rlm/transport.py +640 -0
- better_rlm/tui.py +621 -0
- better_rlm/version.py +42 -0
- better_rlm-0.3.0.dist-info/METADATA +1131 -0
- better_rlm-0.3.0.dist-info/RECORD +64 -0
- better_rlm-0.3.0.dist-info/WHEEL +5 -0
- better_rlm-0.3.0.dist-info/entry_points.txt +2 -0
- better_rlm-0.3.0.dist-info/licenses/LICENSE +22 -0
- better_rlm-0.3.0.dist-info/licenses/NOTICE +17 -0
- better_rlm-0.3.0.dist-info/top_level.txt +2 -0
- rlm/__init__.py +17 -0
- rlm/clients/__init__.py +59 -0
- rlm/clients/anthropic.py +112 -0
- rlm/clients/azure_openai.py +144 -0
- rlm/clients/base_lm.py +47 -0
- rlm/clients/gemini.py +164 -0
- rlm/clients/openai.py +203 -0
- rlm/clients/portkey.py +94 -0
- rlm/core/__init__.py +0 -0
- rlm/core/comms_utils.py +267 -0
- rlm/core/lm_handler.py +266 -0
- rlm/core/rlm.py +1040 -0
- rlm/core/types.py +309 -0
- rlm/environments/__init__.py +82 -0
- rlm/environments/base_env.py +388 -0
- rlm/environments/constants.py +32 -0
- rlm/environments/daytona_repl.py +705 -0
- rlm/environments/docker_repl.py +933 -0
- rlm/environments/e2b_repl.py +512 -0
- rlm/environments/ipython_repl.py +1519 -0
- rlm/environments/local_repl.py +612 -0
- rlm/environments/modal_repl.py +515 -0
- rlm/environments/prime_repl.py +601 -0
- rlm/logger/__init__.py +4 -0
- rlm/logger/rlm_logger.py +91 -0
- rlm/logger/verbose.py +538 -0
- rlm/utils/__init__.py +0 -0
- rlm/utils/exceptions.py +125 -0
- rlm/utils/parsing.py +134 -0
- rlm/utils/prompts.py +285 -0
- rlm/utils/rlm_utils.py +12 -0
- 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
|