Skip to content

Commit ff8bb19

Browse files
iamjr15claude
andcommitted
feat: replace Morph with Relace for fast code edits
- Replace _call_morph_api with _call_relace_api using httpx - Add RELACE_API_KEY to config.py - Use Relace Direct REST API at instantapply.endpoint.relace.run - 256k context window, 10k tok/s speed - Better error handling for rate limits, timeouts, auth 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 106d7ea commit ff8bb19

File tree

2 files changed

+66
-63
lines changed

2 files changed

+66
-63
lines changed

backend/agent/tools/sb_files_tool.py

Lines changed: 65 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -688,73 +688,75 @@ async def read_file(self, file_path: str, start_line: int = 1, end_line: int = N
688688
except Exception as e:
689689
return self.fail_response(f"Error reading file: {str(e)}")
690690

691-
async def _call_morph_api(self, file_content: str, code_edit: str, instructions: str, file_path: str) -> Tuple[Optional[str], Optional[str]]:
691+
async def _call_relace_api(self, file_content: str, code_edit: str, instructions: str, file_path: str) -> Tuple[Optional[str], Optional[str]]:
692692
"""
693-
Call Morph API to apply edits to file content.
693+
Call Relace API to apply edits to file content.
694694
Returns a tuple (new_content, error_message).
695695
On success, error_message is None.
696696
On failure, new_content is None.
697697
"""
698+
import httpx
699+
698700
try:
699-
morph_api_key = getattr(config, 'MORPH_API_KEY', None) or os.getenv('MORPH_API_KEY')
700-
openrouter_key = getattr(config, 'OPENROUTER_API_KEY', None) or os.getenv('OPENROUTER_API_KEY')
701-
702-
messages = [{
703-
"role": "user",
704-
"content": f"<instruction>{instructions}</instruction>\n<code>{file_content}</code>\n<update>{code_edit}</update>"
705-
}]
706-
707-
response = None
708-
if morph_api_key:
709-
logger.debug("Using direct Morph API for file editing.")
710-
client = openai.AsyncOpenAI(
711-
api_key=morph_api_key,
712-
base_url="https://api.morphllm.com/v1"
713-
)
714-
response = await client.chat.completions.create(
715-
model="morph-v3-large",
716-
messages=messages,
717-
temperature=0.0,
718-
timeout=30.0
719-
)
720-
elif openrouter_key:
721-
logger.debug("Morph API key not set, falling back to OpenRouter for file editing via litellm.")
722-
response = await litellm.acompletion(
723-
model="openrouter/morph/morph-v3-large",
724-
messages=messages,
725-
api_key=openrouter_key,
726-
api_base="https://openrouter.ai/api/v1",
727-
temperature=0.0,
728-
timeout=30.0
729-
)
730-
else:
731-
error_msg = "No Morph or OpenRouter API key found, cannot perform AI edit."
732-
logger.warning(error_msg)
733-
return None, error_msg
734-
735-
if response and response.choices and len(response.choices) > 0:
736-
content = response.choices[0].message.content.strip()
737-
738-
# Extract code block if wrapped in markdown
739-
if content.startswith("```") and content.endswith("```"):
740-
lines = content.split('\n')
741-
if len(lines) > 2:
742-
content = '\n'.join(lines[1:-1])
743-
744-
return content, None
745-
else:
746-
error_msg = f"Invalid response from Morph/OpenRouter API: {response}"
747-
logger.error(error_msg)
748-
return None, error_msg
749-
701+
relace_api_key = getattr(config, 'RELACE_API_KEY', None) or os.getenv('RELACE_API_KEY')
702+
703+
if not relace_api_key:
704+
return None, "RELACE_API_KEY not configured"
705+
706+
# Use Direct REST API (more explicit response format)
707+
url = "https://instantapply.endpoint.relace.run/v1/code/apply"
708+
709+
headers = {
710+
"Authorization": f"Bearer {relace_api_key}",
711+
"Content-Type": "application/json"
712+
}
713+
714+
payload = {
715+
"initial_code": file_content,
716+
"edit_snippet": code_edit,
717+
"instruction": instructions,
718+
"model": "auto", # Routes to relace-apply-3
719+
"stream": False,
720+
"relace_metadata": {
721+
"file_path": file_path,
722+
"source": "cheatcode-agent"
723+
}
724+
}
725+
726+
async with httpx.AsyncClient(timeout=60.0) as client:
727+
response = await client.post(url, headers=headers, json=payload)
728+
729+
if response.status_code == 200:
730+
result = response.json()
731+
merged_code = result.get("mergedCode")
732+
usage = result.get("usage", {})
733+
734+
logger.info(f"Relace API success - tokens: {usage.get('total_tokens', 'N/A')}")
735+
736+
if merged_code:
737+
return merged_code, None
738+
else:
739+
return None, "Relace API returned empty mergedCode"
740+
741+
elif response.status_code == 429:
742+
return None, "Rate limit exceeded. Please wait and retry."
743+
744+
elif response.status_code == 413:
745+
return None, f"File too large for Relace API (max 50MB). File: {file_path}"
746+
747+
elif response.status_code in [401, 403]:
748+
return None, "Invalid or missing Relace API key"
749+
750+
else:
751+
error_body = response.text
752+
return None, f"Relace API error ({response.status_code}): {error_body[:500]}"
753+
754+
except httpx.TimeoutException:
755+
return None, f"Relace API timeout after 60s for file: {file_path}"
756+
750757
except Exception as e:
751-
error_message = f"AI model call for file edit failed. Exception: {str(e)}"
752-
# Try to get more details from the exception if it's an API error
753-
if hasattr(e, 'response') and hasattr(e.response, 'text'):
754-
error_message += f"\n\nAPI Response Body:\n{e.response.text}"
755-
elif hasattr(e, 'body'): # litellm sometimes puts it in body
756-
error_message += f"\n\nAPI Response Body:\n{e.body}"
757-
logger.error(f"Error calling Morph/OpenRouter API: {error_message}", exc_info=True)
758+
error_message = f"Relace API call failed: {str(e)}"
759+
logger.error(error_message, exc_info=True)
758760
return None, error_message
759761

760762
async def edit_file(self, target_file: str, instructions: str, code_edit: str) -> ToolResult:
@@ -771,9 +773,9 @@ async def edit_file(self, target_file: str, instructions: str, code_edit: str) -
771773
# Read current content
772774
original_content = (await self.sandbox.fs.download_file(full_path)).decode()
773775

774-
# Try Morph AI editing first
775-
logger.info(f"Attempting AI-powered edit for file '{target_file}' with instructions: {instructions[:100]}...")
776-
new_content, error_message = await self._call_morph_api(original_content, code_edit, instructions, target_file)
776+
# Use Relace fast-apply for intelligent code editing
777+
logger.info(f"Attempting Relace fast-apply edit for file '{target_file}' with instructions: {instructions[:100]}...")
778+
new_content, error_message = await self._call_relace_api(original_content, code_edit, instructions, target_file)
777779

778780
if error_message:
779781
return self.fail_response(f"AI editing failed: {error_message}")

backend/utils/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Configuration:
4444
ANTHROPIC_API_KEY: Optional[str] = None
4545
OPENAI_API_KEY: Optional[str] = None
4646
MORPH_API_KEY: Optional[str] = None
47+
RELACE_API_KEY: Optional[str] = None
4748
OPENROUTER_API_KEY: Optional[str] = None
4849
OPENROUTER_API_BASE: Optional[str] = "https://openrouter.ai/api/v1"
4950
OR_SITE_URL: Optional[str] = "https://trycheatcode.com"

0 commit comments

Comments
 (0)