Skip to content

Commit 4bc26e6

Browse files
author
P. Scott DeVos
committed
Handle RateLimit errors.
1 parent 72d4706 commit 4bc26e6

File tree

2 files changed

+73
-34
lines changed

2 files changed

+73
-34
lines changed

Diff for: rplugin/python3/claude_plugin.py

+72-34
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
ABSOLUTE_MAX_CONTEXT_TOKENS = 1024 * 200
5151
DEFAULT_MAX_CONTEXT_TOKENS = ABSOLUTE_MAX_CONTEXT_TOKENS
5252

53+
LIMIT_WINDOW = 60.0
54+
5355
SETTINGS_FILE = 'nvim_claude.json'
5456

5557
DEFAULT_SYSTEM_PROMPT = """
@@ -135,6 +137,7 @@ def __init__(self, nvim):
135137
"system_prompt": DEFAULT_SYSTEM_PROMPT,
136138
"temperature": DEFAULT_TEMPERATURE,
137139
"timeout": DEFAULT_TIMEOUT,
140+
"limit_window": LIMIT_WINDOW,
138141
}
139142

140143
# Load configuration from file
@@ -163,6 +166,7 @@ def __init__(self, nvim):
163166
self.system_prompt = config["system_prompt"]
164167
self.temperature = config["temperature"]
165168
self.timeout = config["timeout"]
169+
self.limit_window = config["limit_window"]
166170
self.claude_client = anthropic.Client(
167171
timeout=Timeout(10.0, read=self.timeout, write=self.timeout))
168172

@@ -176,6 +180,7 @@ def _save_attributes_to_file(self):
176180
"system_prompt": self.system_prompt,
177181
"temperature": self.temperature,
178182
"timeout": self.timeout,
183+
"limit_window": self.limit_window,
179184
}
180185
config_dir = os.path.join(os.path.expanduser('~'), '.config', 'nvim')
181186
os.makedirs(config_dir, exist_ok=True)
@@ -529,9 +534,16 @@ def replace_buffer(match):
529534
timeout=Timeout(10.0, read=self.timeout, write=self.timeout),
530535
stream=True
531536
)
537+
except anthropic.RateLimitError:
538+
self.nvim.out_write(f"Rate limit reached.")
539+
except Exception as e:
540+
self.nvim.err_write(
541+
f"Error generating response:\n{format_exc()}\n")
542+
return
532543

533-
self.nvim.current.buffer.append(["", "<assistant>", ""])
534-
response_text = ""
544+
self.nvim.current.buffer.append(["", "<assistant>", ""])
545+
response_text = ""
546+
try:
535547
for chunk in response_stream:
536548
if chunk.type == "content_block_delta":
537549
response_text += chunk.delta.text
@@ -542,21 +554,29 @@ def replace_buffer(match):
542554
# Move cursor to the last line of the buffer after appending each chunk
543555
self.nvim.current.window.cursor = (
544556
len(self.nvim.current.buffer), 0)
545-
self.nvim.current.buffer.append(["", "</assistant>", "", ""])
557+
except anthropic.RateLimitError:
558+
self.nvim.err_write("Rate limit reached.")
559+
except Exception as e:
560+
self.nvim.err_write(
561+
f"Error generating response:\n{format_exc()}\n")
562+
return
546563

547-
buffer_number = self.nvim.current.buffer.number
548-
if self.buffer_filenames.get(buffer_number):
549-
# Make sure filename is sanitized
550-
sanitized_filename = shlex.quote(
551-
self.buffer_filenames[buffer_number])
552-
else:
553-
# Get a filename if we don't already have one by asking Claude
554-
filename_prompt = """
555-
Based on our conversation, suggest a descriptive filename
556-
for saving this chat. Respond with only the filename,
557-
no explanation. Keep the filename pithy and descriptive.
558-
Extention should be .txt.
559-
"""
564+
self.nvim.current.buffer.append(["", "</assistant>", "", ""])
565+
566+
buffer_number = self.nvim.current.buffer.number
567+
if self.buffer_filenames.get(buffer_number):
568+
# Make sure filename is sanitized
569+
sanitized_filename = shlex.quote(
570+
self.buffer_filenames[buffer_number])
571+
else:
572+
# Get a filename if we don't already have one by asking Claude
573+
filename_prompt = """
574+
Based on our conversation, suggest a descriptive filename
575+
for saving this chat. Respond with only the filename,
576+
no explanation. Keep the filename pithy and descriptive.
577+
Extention should be .txt.
578+
"""
579+
try:
560580
filename_response = completion_method(
561581
model=self.filename_model,
562582
messages=messages + [
@@ -565,24 +585,33 @@ def replace_buffer(match):
565585
],
566586
max_tokens=100
567587
)
588+
except anthropic.RateLimitError:
589+
self.nvim.err_write(
590+
f"Rate limit reached. Sleeping for {self.limit_window} seconds."
591+
)
592+
self.nvim.err_write(
593+
"You can change the limit window with :ClaudeSettings "
594+
"limit_window=<seconds>")
595+
filename_response = "rate_limit_error_on_filename_response.txt"
596+
except Exception as e:
597+
self.nvim.err_write(
598+
f"Error generating filename:\n{format_exc()}\n"
599+
)
600+
return
568601

569-
filename = self._get_filename_from_response(filename_response)
570-
# Escape spaces and special characters in filename
571-
sanitized_filename = shlex.quote(filename)
572-
sanitized_filename = datetime.now().strftime(
573-
"%Y-%m-%d_%H-%M-%S_") + sanitized_filename
574-
self.buffer_filenames[buffer_number] = sanitized_filename
575-
self.nvim.out_write(f"Saving to {sanitized_filename}\n")
576-
cmd = f'write! {sanitized_filename}'
577-
self.nvim.command(cmd)
602+
filename = self._get_filename_from_response(filename_response)
603+
# Escape spaces and special characters in filename
604+
sanitized_filename = shlex.quote(filename)
605+
sanitized_filename = datetime.now().strftime(
606+
"%Y-%m-%d_%H-%M-%S_") + sanitized_filename
607+
self.buffer_filenames[buffer_number] = sanitized_filename
608+
self.nvim.out_write(f"Saving to {sanitized_filename}\n")
609+
cmd = f'write! {sanitized_filename}'
610+
self.nvim.command(cmd)
578611

579-
# Move cursor to the last line of the buffer after appending response
580-
self.nvim.current.window.cursor = (
581-
len(self.nvim.current.buffer), 0)
582-
583-
except Exception as e:
584-
self.nvim.err_write(
585-
f"Error generating response:\n{format_exc()}\n")
612+
# Move cursor to the last line of the buffer after appending response
613+
self.nvim.current.window.cursor = (
614+
len(self.nvim.current.buffer), 0)
586615

587616
def get_claude_models(self) -> List[str]:
588617
"""Get a list of available Anthropic models."""
@@ -916,6 +945,7 @@ def load_claude_settings_command(self, args: List[str]) -> None:
916945
self.max_context_tokens = DEFAULT_MAX_CONTEXT_TOKENS
917946
self.temperature = DEFAULT_TEMPERATURE
918947
self.timeout = DEFAULT_TIMEOUT
948+
self.limit_window = LIMIT_WINDOW
919949
self._save_attributes_to_file()
920950
self.nvim.out_write("Settings restored to defaults.\n")
921951
elif args[0].lower() == 'save':
@@ -925,7 +955,7 @@ def load_claude_settings_command(self, args: List[str]) -> None:
925955
setting, value = [s.strip() for s in args[0].split('=')]
926956
if setting not in [
927957
'model', 'filename_model', 'max_tokens', 'max_context_tokens',
928-
'truncate', 'temperature', 'timeout'
958+
'truncate', 'temperature', 'timeout', 'limit_window'
929959
]:
930960
self.nvim.err_write(
931961
f"Error: {setting} is not a valid setting.\n")
@@ -983,12 +1013,19 @@ def load_claude_settings_command(self, args: List[str]) -> None:
9831013
"Error: timeout must be between 0.0 and 600.0.\n")
9841014
return
9851015
self.timeout = value
1016+
elif setting == 'limit_window':
1017+
value = float(value)
1018+
if value < 0.0 or value > 600.0: # 0 to 10 minutes
1019+
self.nvim.err_write(
1020+
"Error: limit window must be between 0.0 and 600.0.\n")
1021+
return
1022+
self.limit_window = value
9861023
else:
9871024
self.nvim.err_write(
9881025
"Invalid argument. Use 'defaults' or 'save' or "
9891026
"'model=...', 'filename_model=...', 'max_tokens=...', "
9901027
"'max_context_tokens=...', 'truncate=...', 'temperature=...', "
991-
"'timeout=...'\n")
1028+
"'timeout=...', 'limit_window=...'\n")
9921029
return
9931030
self.nvim.out_write(
9941031
f"Current settings:\n"
@@ -999,6 +1036,7 @@ def load_claude_settings_command(self, args: List[str]) -> None:
9991036
f"truncate_conversation: {self.truncate_conversation}\n"
10001037
f"temperature: {self.temperature}\n"
10011038
f"timeout: {self.timeout}\n"
1039+
f"limit_window: {self.limit_window}\n"
10021040
)
10031041

10041042
@pynvim.command('ClaudeSettings', nargs='?', sync=True)

Diff for: tests/test_claude_plugin.py

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def test__save_attributes_to_file(self, mock_json_dump, mock_open, mock_makedirs
102102
"system_prompt": "Test prompt",
103103
"temperature": 0.5,
104104
"timeout": 300.0,
105+
"limit_window": 60.0
105106
}, mock_file, indent=2)
106107

107108
def test__extract_code_blocks__multiple_blocks(self):

0 commit comments

Comments
 (0)