Skip to content

Commit a443acd

Browse files
authored
Merge branch 'main' into prime-sandboxes
2 parents 9fc2568 + cb9cab7 commit a443acd

15 files changed

Lines changed: 1041 additions & 51 deletions

‎rlm/clients/__init__.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ def get_client(
3636

3737
backend_kwargs.setdefault("base_url", "https://openrouter.ai/api/v1")
3838
return OpenAIClient(**backend_kwargs)
39+
elif backend == "vercel":
40+
from rlm.clients.openai import OpenAIClient
41+
42+
backend_kwargs.setdefault("base_url", "https://ai-gateway.vercel.sh/v1")
43+
return OpenAIClient(**backend_kwargs)
3944
elif backend == "litellm":
4045
from rlm.clients.litellm import LiteLLMClient
4146

@@ -54,5 +59,5 @@ def get_client(
5459
return AzureOpenAIClient(**backend_kwargs)
5560
else:
5661
raise ValueError(
57-
f"Unknown backend: {backend}. Supported backends: ['openai', 'vllm', 'portkey', 'openrouter', 'litellm', 'anthropic', 'azure_openai', 'gemini']"
62+
f"Unknown backend: {backend}. Supported backends: ['openai', 'vllm', 'portkey', 'openrouter', 'litellm', 'anthropic', 'azure_openai', 'gemini', 'vercel']"
5863
)

‎rlm/clients/azure_openai.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def __init__(
3939
if api_version is None:
4040
api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-01")
4141

42+
if azure_deployment is None:
43+
azure_deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT")
44+
4245
if azure_endpoint is None:
4346
raise ValueError(
4447
"azure_endpoint is required for Azure OpenAI client. "

‎rlm/clients/openai.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# Load API keys from environment variables
1414
DEFAULT_OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
1515
DEFAULT_OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
16+
DEFAULT_VERCEL_API_KEY = os.getenv("AI_GATEWAY_API_KEY")
1617
DEFAULT_PRIME_INTELLECT_BASE_URL = "https://api.pinference.ai/api/v1/"
1718

1819

@@ -35,6 +36,8 @@ def __init__(
3536
api_key = DEFAULT_OPENAI_API_KEY
3637
elif base_url == "https://openrouter.ai/api/v1":
3738
api_key = DEFAULT_OPENROUTER_API_KEY
39+
elif base_url == "https://ai-gateway.vercel.sh/v1":
40+
api_key = DEFAULT_VERCEL_API_KEY
3841

3942
# For vLLM, set base_url to local vLLM server address.
4043
self.client = openai.OpenAI(api_key=api_key, base_url=base_url)
@@ -84,7 +87,7 @@ async def acompletion(
8487
raise ValueError("Model name is required for OpenAI client.")
8588

8689
extra_body = {}
87-
if self.base_url == DEFAULT_PRIME_INTELLECT_BASE_URL:
90+
if self.client.base_url == DEFAULT_PRIME_INTELLECT_BASE_URL:
8891
extra_body["usage"] = {"include": True}
8992

9093
response = await self.async_client.chat.completions.create(

‎rlm/core/rlm.py‎

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
RLMIteration,
1414
RLMMetadata,
1515
)
16-
from rlm.environments import BaseEnv, get_environment
16+
from rlm.environments import BaseEnv, SupportsPersistence, get_environment
1717
from rlm.logger import RLMLogger, VerbosePrinter
1818
from rlm.utils.parsing import (
1919
find_code_blocks,
@@ -51,6 +51,7 @@ def __init__(
5151
other_backend_kwargs: list[dict[str, Any]] | None = None,
5252
logger: RLMLogger | None = None,
5353
verbose: bool = False,
54+
persistent: bool = False,
5455
):
5556
"""
5657
Args:
@@ -66,6 +67,7 @@ def __init__(
6667
other_backend_kwargs: The kwargs to pass to the other client backends (ordered to match other_backends).
6768
logger: The logger to use for the RLM.
6869
verbose: Whether to print verbose output in rich to console.
70+
persistent: If True, reuse the environment across completion() calls for multi-turn conversations.
6971
"""
7072
# Store config for spawning per-completion
7173
self.backend = backend
@@ -84,6 +86,14 @@ def __init__(
8486
self.logger = logger
8587
self.verbose = VerbosePrinter(enabled=verbose)
8688

89+
# Persistence support
90+
self.persistent = persistent
91+
self._persistent_env: SupportsPersistence | None = None
92+
93+
# Validate persistence support at initialization
94+
if self.persistent:
95+
self._validate_persistent_environment_support()
96+
8797
# Log metadata if logger is provided
8898
if self.logger or verbose:
8999
metadata = RLMMetadata(
@@ -108,7 +118,9 @@ def __init__(
108118
def _spawn_completion_context(self, prompt: str | dict[str, Any]):
109119
"""
110120
Spawn an LM handler and environment for a single completion call.
111-
Cleans up both when the context exits.
121+
122+
When persistent=True, the environment is reused across calls.
123+
When persistent=False (default), creates fresh environment each call.
112124
"""
113125
# Create client and wrap in handler
114126
client: BaseLM = get_client(self.backend, self.backend_kwargs)
@@ -122,20 +134,32 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
122134

123135
lm_handler.start()
124136

125-
# Pass handler address to environment so it can make llm_query() calls
126-
env_kwargs = self.environment_kwargs.copy()
127-
env_kwargs["lm_handler_address"] = (lm_handler.host, lm_handler.port)
128-
env_kwargs["context_payload"] = prompt
137+
# Environment: reuse if persistent, otherwise create fresh
138+
if self.persistent and self._persistent_env is not None:
139+
environment = self._persistent_env
140+
# Defensive check: ensure environment supports persistence methods
141+
if not self._env_supports_persistence(environment):
142+
raise RuntimeError(
143+
f"Persistent environment of type '{type(environment).__name__}' does not "
144+
f"implement required methods (update_handler_address, add_context, get_context_count). "
145+
f"This should have been caught at initialization."
146+
)
147+
environment.update_handler_address((lm_handler.host, lm_handler.port))
148+
environment.add_context(prompt)
149+
else:
150+
env_kwargs = self.environment_kwargs.copy()
151+
env_kwargs["lm_handler_address"] = (lm_handler.host, lm_handler.port)
152+
env_kwargs["context_payload"] = prompt
153+
environment: BaseEnv = get_environment(self.environment_type, env_kwargs)
129154

130-
# Initialize the environment
131-
environment: BaseEnv = get_environment(self.environment_type, env_kwargs)
155+
if self.persistent:
156+
self._persistent_env = environment
132157

133158
try:
134159
yield lm_handler, environment
135160
finally:
136-
# Cleanup
137161
lm_handler.stop()
138-
if hasattr(environment, "cleanup"):
162+
if not self.persistent and hasattr(environment, "cleanup"):
139163
environment.cleanup()
140164

141165
def _setup_prompt(self, prompt: str | dict[str, Any]) -> list[dict[str, Any]]:
@@ -177,7 +201,19 @@ def completion(
177201

178202
for i in range(self.max_iterations):
179203
# Current prompt = message history + additional prompt suffix
180-
current_prompt = message_history + [build_user_prompt(root_prompt, i)]
204+
context_count = (
205+
environment.get_context_count()
206+
if isinstance(environment, SupportsPersistence)
207+
else 1
208+
)
209+
history_count = (
210+
environment.get_history_count()
211+
if isinstance(environment, SupportsPersistence)
212+
else 0
213+
)
214+
current_prompt = message_history + [
215+
build_user_prompt(root_prompt, i, context_count, history_count)
216+
]
181217

182218
iteration: RLMIteration = self._completion_turn(
183219
prompt=current_prompt,
@@ -201,6 +237,11 @@ def completion(
201237
usage = lm_handler.get_usage_summary()
202238
self.verbose.print_final_answer(final_answer)
203239
self.verbose.print_summary(i + 1, time_end - time_start, usage.to_dict())
240+
241+
# Store message history in persistent environment
242+
if self.persistent and isinstance(environment, SupportsPersistence):
243+
environment.add_history(message_history)
244+
204245
return RLMChatCompletion(
205246
root_model=self.backend_kwargs.get("model_name", "unknown")
206247
if self.backend_kwargs
@@ -223,6 +264,11 @@ def completion(
223264
usage = lm_handler.get_usage_summary()
224265
self.verbose.print_final_answer(final_answer)
225266
self.verbose.print_summary(self.max_iterations, time_end - time_start, usage.to_dict())
267+
268+
# Store message history in persistent environment
269+
if self.persistent and isinstance(environment, SupportsPersistence):
270+
environment.add_history(message_history)
271+
226272
return RLMChatCompletion(
227273
root_model=self.backend_kwargs.get("model_name", "unknown")
228274
if self.backend_kwargs
@@ -292,3 +338,47 @@ def _fallback_answer(self, message: str | dict[str, Any]) -> str:
292338
client: BaseLM = get_client(self.backend, self.backend_kwargs)
293339
response = client.completion(message)
294340
return response
341+
342+
def _validate_persistent_environment_support(self) -> None:
343+
"""
344+
Validate that the configured environment type supports persistent mode.
345+
346+
Persistent mode requires environments to implement:
347+
- update_handler_address(address): Update LM handler address between calls
348+
- add_context(payload, index): Add new context for multi-turn conversations
349+
- get_context_count(): Return the number of loaded contexts
350+
351+
Currently only 'local' (LocalREPL) supports these methods.
352+
353+
Raises:
354+
ValueError: If the environment type does not support persistent mode.
355+
"""
356+
# Known environments that support persistence
357+
persistent_supported_environments = {"local"}
358+
359+
if self.environment_type not in persistent_supported_environments:
360+
raise ValueError(
361+
f"persistent=True is not supported for environment type '{self.environment_type}'. "
362+
f"Persistent mode requires environments that implement update_handler_address(), "
363+
f"add_context(), and get_context_count(). "
364+
f"Supported environments: {sorted(persistent_supported_environments)}"
365+
)
366+
367+
@staticmethod
368+
def _env_supports_persistence(env: BaseEnv) -> bool:
369+
"""Check if an environment instance supports persistent mode methods."""
370+
return isinstance(env, SupportsPersistence)
371+
372+
def close(self) -> None:
373+
"""Clean up persistent environment. Call when done with multi-turn conversations."""
374+
if self._persistent_env is not None:
375+
if hasattr(self._persistent_env, "cleanup"):
376+
self._persistent_env.cleanup()
377+
self._persistent_env = None
378+
379+
def __enter__(self) -> "RLM":
380+
return self
381+
382+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
383+
self.close()
384+
return False

‎rlm/core/types.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
from typing import Any, Literal
44

55
ClientBackend = Literal[
6-
"openai", "portkey", "openrouter", "vllm", "litellm", "anthropic", "azure_openai", "gemini"
6+
"openai",
7+
"portkey",
8+
"openrouter",
9+
"vercel",
10+
"vllm",
11+
"litellm",
12+
"anthropic",
13+
"azure_openai",
14+
"gemini",
715
]
816
EnvironmentType = Literal["local", "docker", "modal", "prime"]
917

‎rlm/environments/__init__.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from typing import Any, Literal
22

3-
from rlm.environments.base_env import BaseEnv
3+
from rlm.environments.base_env import BaseEnv, SupportsPersistence
44
from rlm.environments.local_repl import LocalREPL
55

6+
__all__ = ["BaseEnv", "LocalREPL", "SupportsPersistence", "get_environment"]
7+
68

79
def get_environment(
810
environment: Literal["local", "modal", "docker", "prime"],

0 commit comments

Comments
 (0)