merge in openai upgrade

This commit is contained in:
Joshua Vial 2023-12-11 20:43:18 +13:00
commit fe9423d7b8
23 changed files with 304 additions and 201 deletions

View file

@ -42,6 +42,7 @@ def wrap_fence(name):
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'} IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
class Coder: class Coder:
client = None
abs_fnames = None abs_fnames = None
repo = None repo = None
last_aider_commit_hash = None last_aider_commit_hash = None
@ -58,6 +59,7 @@ class Coder:
main_model=None, main_model=None,
edit_format=None, edit_format=None,
io=None, io=None,
client=None,
skip_model_availabily_check=False, skip_model_availabily_check=False,
**kwargs, **kwargs,
): ):
@ -67,26 +69,28 @@ class Coder:
main_model = models.GPT4 main_model = models.GPT4
if not skip_model_availabily_check and not main_model.always_available: if not skip_model_availabily_check and not main_model.always_available:
if not check_model_availability(io, main_model): if not check_model_availability(io, client, main_model):
fallback_model = models.GPT35_1106
if main_model != models.GPT4: if main_model != models.GPT4:
io.tool_error( io.tool_error(
f"API key does not support {main_model.name}, falling back to" f"API key does not support {main_model.name}, falling back to"
f" {models.GPT35_16k.name}" f" {fallback_model.name}"
) )
main_model = models.GPT35_16k main_model = fallback_model
if edit_format is None: if edit_format is None:
edit_format = main_model.edit_format edit_format = main_model.edit_format
if edit_format == "diff": if edit_format == "diff":
return EditBlockCoder(main_model, io, **kwargs) return EditBlockCoder(client, main_model, io, **kwargs)
elif edit_format == "whole": elif edit_format == "whole":
return WholeFileCoder(main_model, io, **kwargs) return WholeFileCoder(client, main_model, io, **kwargs)
else: else:
raise ValueError(f"Unknown edit format {edit_format}") raise ValueError(f"Unknown edit format {edit_format}")
def __init__( def __init__(
self, self,
client,
main_model, main_model,
io, io,
fnames=None, fnames=None,
@ -105,6 +109,8 @@ class Coder:
voice_language=None, voice_language=None,
aider_ignore_file=None, aider_ignore_file=None,
): ):
self.client = client
if not fnames: if not fnames:
fnames = [] fnames = []
@ -161,7 +167,9 @@ class Coder:
if use_git: if use_git:
try: try:
self.repo = GitRepo(self.io, fnames, git_dname, aider_ignore_file) self.repo = GitRepo(
self.io, fnames, git_dname, aider_ignore_file, client=self.client
)
self.root = self.repo.root self.root = self.repo.root
except FileNotFoundError: except FileNotFoundError:
self.repo = None self.repo = None
@ -192,6 +200,7 @@ class Coder:
self.io.tool_output(f"Added {fname} to the chat.") self.io.tool_output(f"Added {fname} to the chat.")
self.summarizer = ChatSummary( self.summarizer = ChatSummary(
self.client,
models.Model.weak_model(), models.Model.weak_model(),
self.main_model.max_chat_history_tokens, self.main_model.max_chat_history_tokens,
) )
@ -305,6 +314,13 @@ class Coder:
def get_files_messages(self): def get_files_messages(self):
all_content = "" all_content = ""
repo_content = self.get_repo_map()
if repo_content:
if all_content:
all_content += "\n"
all_content += repo_content
if self.abs_fnames: if self.abs_fnames:
files_content = self.gpt_prompts.files_content_prefix files_content = self.gpt_prompts.files_content_prefix
files_content += self.get_files_content() files_content += self.get_files_content()
@ -313,12 +329,6 @@ class Coder:
all_content += files_content all_content += files_content
repo_content = self.get_repo_map()
if repo_content:
if all_content:
all_content += "\n"
all_content += repo_content
files_messages = [ files_messages = [
dict(role="user", content=all_content), dict(role="user", content=all_content),
dict(role="assistant", content="Ok."), dict(role="assistant", content="Ok."),
@ -500,7 +510,7 @@ class Coder:
interrupted = self.send(messages, functions=self.functions) interrupted = self.send(messages, functions=self.functions)
except ExhaustedContextWindow: except ExhaustedContextWindow:
exhausted = True exhausted = True
except openai.error.InvalidRequestError as err: except openai.BadRequestError as err:
if "maximum context length" in str(err): if "maximum context length" in str(err):
exhausted = True exhausted = True
else: else:
@ -617,7 +627,9 @@ class Coder:
interrupted = False interrupted = False
try: try:
hash_object, completion = send_with_retries(model, messages, functions, self.stream) hash_object, completion = send_with_retries(
self.client, model, messages, functions, self.stream
)
self.chat_completion_call_hashes.append(hash_object.hexdigest()) self.chat_completion_call_hashes.append(hash_object.hexdigest())
if self.stream: if self.stream:
@ -971,9 +983,16 @@ class Coder:
return True return True
def check_model_availability(io, main_model): def check_model_availability(io, client, main_model):
available_models = openai.Model.list() try:
model_ids = sorted(model.id for model in available_models["data"]) available_models = client.models.list()
except openai.NotFoundError:
# Azure sometimes returns 404?
# https://discord.com/channels/1131200896827654144/1182327371232186459
io.tool_error("Unable to list available models, proceeding with {main_model.name}")
return True
model_ids = sorted(model.id for model in available_models)
if main_model.name in model_ids: if main_model.name in model_ids:
return True return True

View file

@ -182,7 +182,7 @@ If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
files_no_full_files = "I am not sharing any *read-write* files yet." files_no_full_files = "I am not sharing any *read-write* files yet."
repo_content_prefix = """Below here are summaries of other files present in this git repository. repo_content_prefix = """Below here are summaries of files present in the user's git repository.
Do not propose changes to these files, they are *read-only*. Do not propose changes to these files, they are *read-only*.
To make a file *read-write*, ask the user to *add it to the chat*. To make a file *read-write*, ask the user to *add it to the chat*.
""" """

View file

@ -471,7 +471,7 @@ class Commands:
if not self.voice: if not self.voice:
try: try:
self.voice = voice.Voice() self.voice = voice.Voice(self.coder.client)
except voice.SoundDeviceError: except voice.SoundDeviceError:
self.io.tool_error( self.io.tool_error(
"Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?" "Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?"

View file

@ -7,7 +7,8 @@ from aider.sendchat import simple_send_with_retries
class ChatSummary: class ChatSummary:
def __init__(self, model=models.Model.weak_model(), max_tokens=1024): def __init__(self, client, model=models.Model.weak_model(), max_tokens=1024):
self.client = client
self.tokenizer = model.tokenizer self.tokenizer = model.tokenizer
self.max_tokens = max_tokens self.max_tokens = max_tokens
self.model = model self.model = model
@ -84,7 +85,7 @@ class ChatSummary:
dict(role="user", content=content), dict(role="user", content=content),
] ]
summary = simple_send_with_retries(self.model.name, messages) summary = simple_send_with_retries(self.client, self.model.name, messages)
if summary is None: if summary is None:
raise ValueError(f"summarizer unexpectedly failed for {self.model.name}") raise ValueError(f"summarizer unexpectedly failed for {self.model.name}")
summary = prompts.summary_prefix + summary summary = prompts.summary_prefix + summary

View file

@ -157,12 +157,13 @@ def main(argv=None, input=None, output=None, force_git_root=None):
default=False, default=False,
help="Override to skip model availability check (default: False)", help="Override to skip model availability check (default: False)",
) )
default_3_model = models.GPT35_1106
core_group.add_argument( core_group.add_argument(
"-3", "-3",
action="store_const", action="store_const",
dest="model", dest="model",
const=models.GPT35_16k.name, const=default_3_model.name,
help=f"Use {models.GPT35_16k.name} model for the main chat (gpt-4 is better)", help=f"Use {default_3_model.name} model for the main chat (gpt-4 is better)",
) )
core_group.add_argument( core_group.add_argument(
"--voice-language", "--voice-language",
@ -176,27 +177,22 @@ def main(argv=None, input=None, output=None, force_git_root=None):
model_group.add_argument( model_group.add_argument(
"--openai-api-base", "--openai-api-base",
metavar="OPENAI_API_BASE", metavar="OPENAI_API_BASE",
help="Specify the openai.api_base (default: https://api.openai.com/v1)", help="Specify the api base url",
) )
model_group.add_argument( model_group.add_argument(
"--openai-api-type", "--openai-api-type",
metavar="OPENAI_API_TYPE", metavar="OPENAI_API_TYPE",
help="Specify the openai.api_type", help="Specify the api_type",
) )
model_group.add_argument( model_group.add_argument(
"--openai-api-version", "--openai-api-version",
metavar="OPENAI_API_VERSION", metavar="OPENAI_API_VERSION",
help="Specify the openai.api_version", help="Specify the api_version",
) )
model_group.add_argument( model_group.add_argument(
"--openai-api-deployment-id", "--openai-api-deployment-id",
metavar="OPENAI_API_DEPLOYMENT_ID", metavar="OPENAI_API_DEPLOYMENT_ID",
help="Specify the deployment_id arg to be passed to openai.ChatCompletion.create()", help="Specify the deployment_id",
)
model_group.add_argument(
"--openai-api-engine",
metavar="OPENAI_API_ENGINE",
help="Specify the engine arg to be passed to openai.ChatCompletion.create()",
) )
model_group.add_argument( model_group.add_argument(
"--edit-format", "--edit-format",
@ -380,6 +376,12 @@ def main(argv=None, input=None, output=None, force_git_root=None):
metavar="COMMAND", metavar="COMMAND",
help="Specify a single message to send GPT, process reply then exit (disables chat mode)", help="Specify a single message to send GPT, process reply then exit (disables chat mode)",
) )
other_group.add_argument(
"--message-file",
"-f",
metavar="MESSAGE_FILE",
help="Specify a file containing the message to send GPT, process reply, then exit (disables chat mode)",
)
other_group.add_argument( other_group.add_argument(
"--encoding", "--encoding",
default="utf-8", default="utf-8",
@ -492,23 +494,34 @@ def main(argv=None, input=None, output=None, force_git_root=None):
) )
return 1 return 1
openai.api_key = args.openai_api_key if args.openai_api_type == "azure":
for attr in ("base", "type", "version", "deployment_id", "engine"): client = openai.AzureOpenAI(
arg_key = f"openai_api_{attr}" api_key=args.openai_api_key,
val = getattr(args, arg_key) azure_endpoint=args.openai_api_base,
if val is not None: api_version=args.openai_api_version,
mod_key = f"api_{attr}" azure_deployment=args.openai_api_deployment_id,
setattr(openai, mod_key, val) )
io.tool_output(f"Setting openai.{mod_key}={val}") else:
kwargs = dict()
if args.openai_api_base:
kwargs["base_url"] = args.openai_api_base
if "openrouter.ai" in args.openai_api_base:
kwargs["default_headers"] = {
"HTTP-Referer": "http://aider.chat",
"X-Title": "Aider",
}
main_model = models.Model.create(args.model) client = openai.OpenAI(api_key=args.openai_api_key, **kwargs)
main_model = models.Model.create(args.model, client)
try: try:
coder = Coder.create( coder = Coder.create(
main_model, main_model=main_model,
args.edit_format, edit_format=args.edit_format,
io, io=io,
args.skip_model_availability_check, skip_model_availabily_check=args.skip_model_availability_check,
client=client,
## ##
fnames=fnames, fnames=fnames,
git_dname=git_dname, git_dname=git_dname,
@ -560,8 +573,20 @@ def main(argv=None, input=None, output=None, force_git_root=None):
io.tool_error(f"Git working dir: {git_root}") io.tool_error(f"Git working dir: {git_root}")
if args.message: if args.message:
io.add_to_input_history(args.message)
io.tool_output() io.tool_output()
coder.run(with_message=args.message) coder.run(with_message=args.message)
elif args.message_file:
try:
message_from_file = io.read_text(args.message_file)
io.tool_output()
coder.run(with_message=message_from_file)
except FileNotFoundError:
io.tool_error(f"Message file not found: {args.message_file}")
return 1
except IOError as e:
io.tool_error(f"Error reading message file: {e}")
return 1
else: else:
coder.run() coder.run()

View file

@ -4,6 +4,7 @@ from .openrouter import OpenRouterModel
GPT4 = Model.create("gpt-4") GPT4 = Model.create("gpt-4")
GPT35 = Model.create("gpt-3.5-turbo") GPT35 = Model.create("gpt-3.5-turbo")
GPT35_1106 = Model.create("gpt-3.5-turbo-1106")
GPT35_16k = Model.create("gpt-3.5-turbo-16k") GPT35_16k = Model.create("gpt-3.5-turbo-16k")
__all__ = [ __all__ = [

View file

@ -1,10 +1,8 @@
import json import json
import math import math
import openai
from PIL import Image from PIL import Image
class Model: class Model:
name = None name = None
edit_format = None edit_format = None
@ -20,12 +18,12 @@ class Model:
completion_price = None completion_price = None
@classmethod @classmethod
def create(cls, name): def create(cls, name, client=None):
from .openai import OpenAIModel from .openai import OpenAIModel
from .openrouter import OpenRouterModel from .openrouter import OpenRouterModel
if "openrouter.ai" in openai.api_base: if client and client.base_url.host == "openrouter.ai":
return OpenRouterModel(name) return OpenRouterModel(client, name)
return OpenAIModel(name) return OpenAIModel(name)
def __str__(self): def __str__(self):
@ -37,11 +35,11 @@ class Model:
@staticmethod @staticmethod
def weak_model(): def weak_model():
return Model.create("gpt-3.5-turbo") return Model.create("gpt-3.5-turbo-1106")
@staticmethod @staticmethod
def commit_message_models(): def commit_message_models():
return [Model.create("gpt-3.5-turbo"), Model.create("gpt-3.5-turbo-16k")] return [Model.weak_model()]
def token_count(self, messages): def token_count(self, messages):
if not self.tokenizer: if not self.tokenizer:
@ -61,8 +59,6 @@ class Model:
:param fname: The filename of the image. :param fname: The filename of the image.
:return: The token cost for the image. :return: The token cost for the image.
""" """
# Placeholder for image size retrieval logic
# TODO: Implement the logic to retrieve the image size from the file
width, height = self.get_image_size(fname) width, height = self.get_image_size(fname)
# If the image is larger than 2048 in any dimension, scale it down to fit within 2048x2048 # If the image is larger than 2048 in any dimension, scale it down to fit within 2048x2048

View file

@ -55,6 +55,7 @@ class OpenAIModel(Model):
if self.is_gpt35(): if self.is_gpt35():
self.edit_format = "whole" self.edit_format = "whole"
self.always_available = True self.always_available = True
self.send_undo_reply = False
if self.name == "gpt-3.5-turbo-1106": if self.name == "gpt-3.5-turbo-1106":
self.prompt_price = 0.001 self.prompt_price = 0.001

View file

@ -1,4 +1,3 @@
import openai
import tiktoken import tiktoken
from .model import Model from .model import Model
@ -7,13 +6,9 @@ cached_model_details = None
class OpenRouterModel(Model): class OpenRouterModel(Model):
def __init__(self, name): def __init__(self, client, name):
if name == "gpt-4": if name.startswith("gpt-4") or name.startswith("gpt-3.5-turbo"):
name = "openai/gpt-4" name = "openai/" + name
elif name == "gpt-3.5-turbo":
name = "openai/gpt-3.5-turbo"
elif name == "gpt-3.5-turbo-16k":
name = "openai/gpt-3.5-turbo-16k"
self.name = name self.name = name
self.edit_format = edit_format_for_model(name) self.edit_format = edit_format_for_model(name)
@ -24,7 +19,7 @@ class OpenRouterModel(Model):
global cached_model_details global cached_model_details
if cached_model_details is None: if cached_model_details is None:
cached_model_details = openai.Model.list().data cached_model_details = client.models.list().data
found = next( found = next(
(details for details in cached_model_details if details.get("id") == name), None (details for details in cached_model_details if details.get("id") == name), None
) )

View file

@ -16,7 +16,8 @@ class GitRepo:
aider_ignore_spec = None aider_ignore_spec = None
aider_ignore_ts = 0 aider_ignore_ts = 0
def __init__(self, io, fnames, git_dname, aider_ignore_file=None): def __init__(self, io, fnames, git_dname, aider_ignore_file=None, client=None):
self.client = client
self.io = io self.io = io
if git_dname: if git_dname:
@ -102,9 +103,7 @@ class GitRepo:
def get_commit_message(self, diffs, context): def get_commit_message(self, diffs, context):
if len(diffs) >= 4 * 1024 * 4: if len(diffs) >= 4 * 1024 * 4:
self.io.tool_error( self.io.tool_error("Diff is too large to generate a commit message.")
f"Diff is too large for {models.GPT35.name} to generate a commit message."
)
return return
diffs = "# Diffs:\n" + diffs diffs = "# Diffs:\n" + diffs
@ -120,7 +119,7 @@ class GitRepo:
] ]
for model in models.Model.commit_message_models(): for model in models.Model.commit_message_models():
commit_message = simple_send_with_retries(model.name, messages) commit_message = simple_send_with_retries(self.client, model.name, messages)
if commit_message: if commit_message:
break break

View file

@ -2,17 +2,13 @@ import hashlib
import json import json
import backoff import backoff
import httpx
import openai import openai
import requests
# from diskcache import Cache # from diskcache import Cache
from openai.error import ( from openai import APIConnectionError, InternalServerError, RateLimitError
APIConnectionError,
APIError, from aider.dump import dump # noqa: F401
RateLimitError,
ServiceUnavailableError,
Timeout,
)
CACHE_PATH = "~/.aider.send.cache.v1" CACHE_PATH = "~/.aider.send.cache.v1"
CACHE = None CACHE = None
@ -22,19 +18,20 @@ CACHE = None
@backoff.on_exception( @backoff.on_exception(
backoff.expo, backoff.expo,
( (
Timeout, InternalServerError,
APIError,
ServiceUnavailableError,
RateLimitError, RateLimitError,
APIConnectionError, APIConnectionError,
requests.exceptions.ConnectionError, httpx.ConnectError,
), ),
max_tries=10, max_tries=10,
on_backoff=lambda details: print( on_backoff=lambda details: print(
f"{details.get('exception','Exception')}\nRetry in {details['wait']:.1f} seconds." f"{details.get('exception','Exception')}\nRetry in {details['wait']:.1f} seconds."
), ),
) )
def send_with_retries(model_name, messages, functions, stream): def send_with_retries(client, model_name, messages, functions, stream):
if not client:
raise ValueError("No openai client provided")
kwargs = dict( kwargs = dict(
model=model_name, model=model_name,
messages=messages, messages=messages,
@ -44,17 +41,8 @@ def send_with_retries(model_name, messages, functions, stream):
if functions is not None: if functions is not None:
kwargs["functions"] = functions kwargs["functions"] = functions
# we are abusing the openai object to stash these values
if hasattr(openai, "api_deployment_id"):
kwargs["deployment_id"] = openai.api_deployment_id
if hasattr(openai, "api_engine"):
kwargs["engine"] = openai.api_engine
if "openrouter.ai" in openai.api_base:
kwargs["headers"] = {"HTTP-Referer": "http://aider.chat", "X-Title": "Aider"}
# Check conditions to switch to gpt-4-vision-preview # Check conditions to switch to gpt-4-vision-preview
if "openrouter.ai" not in openai.api_base and model_name.startswith("gpt-4"): if client and client.base_url.host != "openrouter.ai" and model_name.startswith("gpt-4"):
if any(isinstance(msg.get("content"), list) and any("image_url" in item for item in msg.get("content") if isinstance(item, dict)) for msg in messages): if any(isinstance(msg.get("content"), list) and any("image_url" in item for item in msg.get("content") if isinstance(item, dict)) for msg in messages):
kwargs['model'] = "gpt-4-vision-preview" kwargs['model'] = "gpt-4-vision-preview"
# looks like gpt-4-vision is limited to max tokens of 4096 # looks like gpt-4-vision is limited to max tokens of 4096
@ -68,7 +56,7 @@ def send_with_retries(model_name, messages, functions, stream):
if not stream and CACHE is not None and key in CACHE: if not stream and CACHE is not None and key in CACHE:
return hash_object, CACHE[key] return hash_object, CACHE[key]
res = openai.ChatCompletion.create(**kwargs) res = client.chat.completions.create(**kwargs)
if not stream and CACHE is not None: if not stream and CACHE is not None:
CACHE[key] = res CACHE[key] = res
@ -76,14 +64,15 @@ def send_with_retries(model_name, messages, functions, stream):
return hash_object, res return hash_object, res
def simple_send_with_retries(model_name, messages): def simple_send_with_retries(client, model_name, messages):
try: try:
_hash, response = send_with_retries( _hash, response = send_with_retries(
client=client,
model_name=model_name, model_name=model_name,
messages=messages, messages=messages,
functions=None, functions=None,
stream=False, stream=False,
) )
return response.choices[0].message.content return response.choices[0].message.content
except (AttributeError, openai.error.InvalidRequestError): except (AttributeError, openai.BadRequestError):
return return

View file

@ -4,7 +4,6 @@ import tempfile
import time import time
import numpy as np import numpy as np
import openai
try: try:
import soundfile as sf import soundfile as sf
@ -27,7 +26,7 @@ class Voice:
threshold = 0.15 threshold = 0.15
def __init__(self): def __init__(self, client):
if sf is None: if sf is None:
raise SoundDeviceError raise SoundDeviceError
try: try:
@ -38,6 +37,8 @@ class Voice:
except (OSError, ModuleNotFoundError): except (OSError, ModuleNotFoundError):
raise SoundDeviceError raise SoundDeviceError
self.client = client
def callback(self, indata, frames, time, status): def callback(self, indata, frames, time, status):
"""This is called (from a separate thread) for each audio block.""" """This is called (from a separate thread) for each audio block."""
rms = np.sqrt(np.mean(indata**2)) rms = np.sqrt(np.mean(indata**2))
@ -88,9 +89,11 @@ class Voice:
file.write(self.q.get()) file.write(self.q.get())
with open(filename, "rb") as fh: with open(filename, "rb") as fh:
transcript = openai.Audio.transcribe("whisper-1", fh, prompt=history, language=language) transcript = self.client.audio.transcriptions.create(
model="whisper-1", file=fh, prompt=history, language=language
)
text = transcript["text"] text = transcript.text
return text return text

View file

@ -30,7 +30,7 @@ from aider.coders import Coder
from aider.dump import dump # noqa: F401 from aider.dump import dump # noqa: F401
from aider.io import InputOutput from aider.io import InputOutput
BENCHMARK_DNAME = Path(os.environ["AIDER_BENCHMARK_DIR"]) BENCHMARK_DNAME = Path(os.environ.get("AIDER_BENCHMARK_DIR", "tmp.benchmarks"))
ORIGINAL_DNAME = BENCHMARK_DNAME / "exercism-python" ORIGINAL_DNAME = BENCHMARK_DNAME / "exercism-python"
@ -631,12 +631,13 @@ def run_test(
show_fnames = ",".join(map(str, fnames)) show_fnames = ",".join(map(str, fnames))
print("fnames:", show_fnames) print("fnames:", show_fnames)
openai.api_key = os.environ["OPENAI_API_KEY"] client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])
coder = Coder.create( coder = Coder.create(
main_model, main_model,
edit_format, edit_format,
io, io,
client=client,
fnames=fnames, fnames=fnames,
use_git=False, use_git=False,
stream=False, stream=False,

View file

@ -1,3 +1,6 @@
#
# pip-compile --output-file=dev-requirements.txt dev-requirements.in
#
pytest pytest
pip-tools pip-tools
lox lox
@ -5,3 +8,4 @@ matplotlib
pandas pandas
typer typer
imgcat imgcat
pre-commit

View file

@ -10,8 +10,10 @@ babel==2.13.1
# via sphinx # via sphinx
build==1.0.3 build==1.0.3
# via pip-tools # via pip-tools
certifi==2023.7.22 certifi==2023.11.17
# via requests # via requests
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.3.2 charset-normalizer==3.3.2
# via requests # via requests
click==8.1.7 click==8.1.7
@ -26,13 +28,19 @@ dill==0.3.7
# via # via
# multiprocess # multiprocess
# pathos # pathos
docutils==0.18.1 distlib==0.3.7
# via virtualenv
docutils==0.20.1
# via # via
# sphinx # sphinx
# sphinx-rtd-theme # sphinx-rtd-theme
fonttools==4.44.0 filelock==3.13.1
# via virtualenv
fonttools==4.46.0
# via matplotlib # via matplotlib
idna==3.4 identify==2.5.32
# via pre-commit
idna==3.6
# via requests # via requests
imagesize==1.4.1 imagesize==1.4.1
# via sphinx # via sphinx
@ -48,11 +56,13 @@ lox==0.11.0
# via -r dev-requirements.in # via -r dev-requirements.in
markupsafe==2.1.3 markupsafe==2.1.3
# via jinja2 # via jinja2
matplotlib==3.8.1 matplotlib==3.8.2
# via -r dev-requirements.in # via -r dev-requirements.in
multiprocess==0.70.15 multiprocess==0.70.15
# via pathos # via pathos
numpy==1.26.1 nodeenv==1.8.0
# via pre-commit
numpy==1.26.2
# via # via
# contourpy # contourpy
# matplotlib # matplotlib
@ -63,7 +73,7 @@ packaging==23.2
# matplotlib # matplotlib
# pytest # pytest
# sphinx # sphinx
pandas==2.1.2 pandas==2.1.3
# via -r dev-requirements.in # via -r dev-requirements.in
pathos==0.3.1 pathos==0.3.1
# via lox # via lox
@ -71,13 +81,17 @@ pillow==10.1.0
# via matplotlib # via matplotlib
pip-tools==7.3.0 pip-tools==7.3.0
# via -r dev-requirements.in # via -r dev-requirements.in
platformdirs==4.1.0
# via virtualenv
pluggy==1.3.0 pluggy==1.3.0
# via pytest # via pytest
pox==0.3.3 pox==0.3.3
# via pathos # via pathos
ppft==1.7.6.7 ppft==1.7.6.7
# via pathos # via pathos
pygments==2.16.1 pre-commit==3.5.0
# via -r dev-requirements.in
pygments==2.17.2
# via sphinx # via sphinx
pyparsing==3.1.1 pyparsing==3.1.1
# via matplotlib # via matplotlib
@ -91,6 +105,8 @@ python-dateutil==2.8.2
# pandas # pandas
pytz==2023.3.post1 pytz==2023.3.post1
# via pandas # via pandas
pyyaml==6.0.1
# via pre-commit
requests==2.31.0 requests==2.31.0
# via sphinx # via sphinx
six==1.16.0 six==1.16.0
@ -106,7 +122,7 @@ sphinx==7.2.6
# sphinxcontrib-jquery # sphinxcontrib-jquery
# sphinxcontrib-qthelp # sphinxcontrib-qthelp
# sphinxcontrib-serializinghtml # sphinxcontrib-serializinghtml
sphinx-rtd-theme==1.3.0 sphinx-rtd-theme==2.0.0
# via lox # via lox
sphinxcontrib-applehelp==1.0.7 sphinxcontrib-applehelp==1.0.7
# via sphinx # via sphinx
@ -128,9 +144,11 @@ typing-extensions==4.8.0
# via typer # via typer
tzdata==2023.3 tzdata==2023.3
# via pandas # via pandas
urllib3==2.0.7 urllib3==2.1.0
# via requests # via requests
wheel==0.41.3 virtualenv==20.25.0
# via pre-commit
wheel==0.42.0
# via pip-tools # via pip-tools
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:

View file

@ -274,13 +274,17 @@ done
You can also script aider from python: You can also script aider from python:
```python ```python
import openai
from aider.coders import Coder from aider.coders import Coder
# Make an openai client
client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])
# This is a list of files to add to the chat # This is a list of files to add to the chat
fnames = ["foo.py"] fnames = ["foo.py"]
# Create a coder object # Create a coder object
coder = Coder.create(fnames=fnames) coder = Coder.create(client=client, fnames=fnames)
# This will execute one instruction on those files and then return # This will execute one instruction on those files and then return
coder.run("make a script that prints hello world") coder.run("make a script that prints hello world")

View file

@ -1,3 +1,6 @@
#
# pip-compile requirements.in
#
configargparse configargparse
GitPython GitPython
openai openai

View file

@ -4,66 +4,67 @@
# #
# pip-compile requirements.in # pip-compile requirements.in
# #
aiohttp==3.8.6 annotated-types==0.6.0
# via openai # via pydantic
aiosignal==1.3.1 anyio==3.7.1
# via aiohttp # via
async-timeout==4.0.3 # httpx
# via aiohttp # openai
attrs==23.1.0 attrs==23.1.0
# via # via
# aiohttp
# jsonschema # jsonschema
# referencing # referencing
backoff==2.2.1 backoff==2.2.1
# via -r requirements.in # via -r requirements.in
certifi==2023.7.22 certifi==2023.11.17
# via requests # via
# httpcore
# httpx
# requests
cffi==1.16.0 cffi==1.16.0
# via # via
# sounddevice # sounddevice
# soundfile # soundfile
charset-normalizer==3.3.2 charset-normalizer==3.3.2
# via # via requests
# aiohttp
# requests
configargparse==1.7 configargparse==1.7
# via -r requirements.in # via -r requirements.in
diskcache==5.6.3 diskcache==5.6.3
# via -r requirements.in # via -r requirements.in
frozenlist==1.4.0 distro==1.8.0
# via # via openai
# aiohttp
# aiosignal
gitdb==4.0.11 gitdb==4.0.11
# via gitpython # via gitpython
gitpython==3.1.40 gitpython==3.1.40
# via -r requirements.in # via -r requirements.in
grep-ast==0.2.4 grep-ast==0.2.4
# via -r requirements.in # via -r requirements.in
idna==3.4 h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
httpx==0.25.2
# via openai
idna==3.6
# via # via
# anyio
# httpx
# requests # requests
# yarl jsonschema==4.20.0
jsonschema==4.19.2
# via -r requirements.in # via -r requirements.in
jsonschema-specifications==2023.7.1 jsonschema-specifications==2023.11.2
# via jsonschema # via jsonschema
markdown-it-py==3.0.0 markdown-it-py==3.0.0
# via rich # via rich
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
multidict==6.0.4
# via
# aiohttp
# yarl
networkx==3.2.1 networkx==3.2.1
# via -r requirements.in # via -r requirements.in
numpy==1.26.1 numpy==1.26.2
# via # via
# -r requirements.in # -r requirements.in
# scipy # scipy
openai==0.28.1 openai==1.3.7
# via -r requirements.in # via -r requirements.in
packaging==23.2 packaging==23.2
# via -r requirements.in # via -r requirements.in
@ -73,49 +74,59 @@ pathspec==0.11.2
# grep-ast # grep-ast
pillow==10.1.0 pillow==10.1.0
# via -r requirements.in # via -r requirements.in
prompt-toolkit==3.0.39 prompt-toolkit==3.0.41
# via -r requirements.in # via -r requirements.in
pycparser==2.21 pycparser==2.21
# via cffi # via cffi
pygments==2.16.1 pydantic==2.5.2
# via openai
pydantic-core==2.14.5
# via pydantic
pygments==2.17.2
# via rich # via rich
pyyaml==6.0.1 pyyaml==6.0.1
# via -r requirements.in # via -r requirements.in
referencing==0.30.2 referencing==0.31.1
# via # via
# jsonschema # jsonschema
# jsonschema-specifications # jsonschema-specifications
regex==2023.10.3 regex==2023.10.3
# via tiktoken # via tiktoken
requests==2.31.0 requests==2.31.0
# via # via tiktoken
# openai rich==13.7.0
# tiktoken
rich==13.6.0
# via -r requirements.in # via -r requirements.in
rpds-py==0.10.6 rpds-py==0.13.2
# via # via
# jsonschema # jsonschema
# referencing # referencing
scipy==1.11.3 scipy==1.11.4
# via -r requirements.in # via -r requirements.in
smmap==5.0.1 smmap==5.0.1
# via gitdb # via gitdb
sniffio==1.3.0
# via
# anyio
# httpx
# openai
sounddevice==0.4.6 sounddevice==0.4.6
# via -r requirements.in # via -r requirements.in
soundfile==0.12.1 soundfile==0.12.1
# via -r requirements.in # via -r requirements.in
tiktoken==0.5.1 tiktoken==0.5.2
# via -r requirements.in # via -r requirements.in
tqdm==4.66.1 tqdm==4.66.1
# via openai # via openai
tree-sitter==0.20.2 tree-sitter==0.20.4
# via tree-sitter-languages # via tree-sitter-languages
tree-sitter-languages==1.8.0 tree-sitter-languages==1.8.0
# via grep-ast # via grep-ast
urllib3==2.0.7 typing-extensions==4.8.0
# via
# openai
# pydantic
# pydantic-core
urllib3==2.1.0
# via requests # via requests
wcwidth==0.2.9 wcwidth==0.2.12
# via prompt-toolkit # via prompt-toolkit
yarl==1.9.2
# via aiohttp

View file

@ -331,22 +331,25 @@ class TestCoder(unittest.TestCase):
# both files should still be here # both files should still be here
self.assertEqual(len(coder.abs_fnames), 2) self.assertEqual(len(coder.abs_fnames), 2)
@patch("aider.coders.base_coder.openai.ChatCompletion.create") def test_run_with_invalid_request_error(self):
def test_run_with_invalid_request_error(self, mock_chat_completion_create):
with ChdirTemporaryDirectory(): with ChdirTemporaryDirectory():
# Mock the IO object # Mock the IO object
mock_io = MagicMock() mock_io = MagicMock()
# Initialize the Coder object with the mocked IO and mocked repo mock_client = MagicMock()
coder = Coder.create(models.GPT4, None, mock_io)
# Set up the mock to raise InvalidRequestError # Initialize the Coder object with the mocked IO and mocked repo
mock_chat_completion_create.side_effect = openai.error.InvalidRequestError( coder = Coder.create(models.GPT4, None, mock_io, client=mock_client)
"Invalid request", "param"
# Set up the mock to raise
mock_client.chat.completions.create.side_effect = openai.BadRequestError(
message="Invalid request",
response=MagicMock(),
body=None,
) )
# Call the run method and assert that InvalidRequestError is raised # Call the run method and assert that InvalidRequestError is raised
with self.assertRaises(openai.error.InvalidRequestError): with self.assertRaises(openai.BadRequestError):
coder.run(with_message="hi") coder.run(with_message="hi")
def test_new_file_edit_one_commit(self): def test_new_file_edit_one_commit(self):

View file

@ -182,6 +182,23 @@ class TestMain(TestCase):
_, kwargs = MockCoder.call_args _, kwargs = MockCoder.call_args
assert kwargs["dirty_commits"] is True assert kwargs["dirty_commits"] is True
def test_message_file_flag(self):
message_file_content = "This is a test message from a file."
message_file_path = tempfile.mktemp()
with open(message_file_path, "w", encoding="utf-8") as message_file:
message_file.write(message_file_content)
with patch("aider.main.Coder.create") as MockCoder:
MockCoder.return_value.run = MagicMock()
main(
["--yes", "--message-file", message_file_path],
input=DummyInput(),
output=DummyOutput(),
)
MockCoder.return_value.run.assert_called_once_with(with_message=message_file_content)
os.remove(message_file_path)
def test_encodings_arg(self): def test_encodings_arg(self):
fname = "foo.py" fname = "foo.py"
@ -196,3 +213,13 @@ class TestMain(TestCase):
MockSend.side_effect = side_effect MockSend.side_effect = side_effect
main(["--yes", fname, "--encoding", "iso-8859-15"]) main(["--yes", fname, "--encoding", "iso-8859-15"])
@patch("aider.main.InputOutput")
@patch("aider.coders.base_coder.Coder.run")
def test_main_message_adds_to_input_history(self, mock_run, MockInputOutput):
test_message = "test message"
mock_io_instance = MockInputOutput.return_value
main(["--message", test_message], input=DummyInput(), output=DummyOutput())
mock_io_instance.add_to_input_history.assert_called_once_with(test_message)

View file

@ -1,5 +1,5 @@
import unittest import unittest
from unittest.mock import patch from unittest.mock import MagicMock
from aider.models import Model, OpenRouterModel from aider.models import Model, OpenRouterModel
@ -12,6 +12,9 @@ class TestModels(unittest.TestCase):
model = Model.create("gpt-3.5-turbo-16k") model = Model.create("gpt-3.5-turbo-16k")
self.assertEqual(model.max_context_tokens, 16 * 1024) self.assertEqual(model.max_context_tokens, 16 * 1024)
model = Model.create("gpt-3.5-turbo-1106")
self.assertEqual(model.max_context_tokens, 16 * 1024)
model = Model.create("gpt-4") model = Model.create("gpt-4")
self.assertEqual(model.max_context_tokens, 8 * 1024) self.assertEqual(model.max_context_tokens, 8 * 1024)
@ -24,13 +27,9 @@ class TestModels(unittest.TestCase):
model = Model.create("gpt-4-32k-2123") model = Model.create("gpt-4-32k-2123")
self.assertEqual(model.max_context_tokens, 32 * 1024) self.assertEqual(model.max_context_tokens, 32 * 1024)
@patch("openai.Model.list") def test_openrouter_model_properties(self):
def test_openrouter_model_properties(self, mock_model_list): client = MagicMock()
import openai client.models.list.return_value = {
old_base = openai.api_base
openai.api_base = "https://openrouter.ai/api/v1"
mock_model_list.return_value = {
"data": [ "data": [
{ {
"id": "openai/gpt-4", "id": "openai/gpt-4",
@ -40,16 +39,15 @@ class TestModels(unittest.TestCase):
} }
] ]
} }
mock_model_list.return_value = type( client.models.list.return_value = type(
"", (), {"data": mock_model_list.return_value["data"]} "", (), {"data": client.models.list.return_value["data"]}
)() )()
model = OpenRouterModel("gpt-4") model = OpenRouterModel(client, "gpt-4")
self.assertEqual(model.name, "openai/gpt-4") self.assertEqual(model.name, "openai/gpt-4")
self.assertEqual(model.max_context_tokens, 8192) self.assertEqual(model.max_context_tokens, 8192)
self.assertEqual(model.prompt_price, 0.06) self.assertEqual(model.prompt_price, 0.06)
self.assertEqual(model.completion_price, 0.12) self.assertEqual(model.completion_price, 0.12)
openai.api_base = old_base
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,41 +1,46 @@
import unittest import unittest
from unittest.mock import patch from unittest.mock import MagicMock, patch
import httpx
import openai import openai
import requests
from aider.sendchat import send_with_retries from aider.sendchat import send_with_retries
class PrintCalled(Exception):
pass
class TestSendChat(unittest.TestCase): class TestSendChat(unittest.TestCase):
@patch("aider.sendchat.openai.ChatCompletion.create")
@patch("builtins.print") @patch("builtins.print")
def test_send_with_retries_rate_limit_error(self, mock_print, mock_chat_completion_create): def test_send_with_retries_rate_limit_error(self, mock_print):
# Set up the mock to raise RateLimitError on mock_client = MagicMock()
# the first call and return None on the second call
mock_chat_completion_create.side_effect = [ # Set up the mock to raise
openai.error.RateLimitError("Rate limit exceeded"), mock_client.chat.completions.create.side_effect = [
openai.RateLimitError(
"rate limit exceeded",
response=MagicMock(),
body=None,
),
None, None,
] ]
# Call the send_with_retries method # Call the send_with_retries method
send_with_retries("model", ["message"], None, False) send_with_retries(mock_client, "model", ["message"], None, False)
# Assert that print was called once
mock_print.assert_called_once() mock_print.assert_called_once()
@patch("aider.sendchat.openai.ChatCompletion.create") @patch("aider.sendchat.openai.ChatCompletion.create")
@patch("builtins.print") @patch("builtins.print")
def test_send_with_retries_connection_error(self, mock_print, mock_chat_completion_create): def test_send_with_retries_connection_error(self, mock_print, mock_chat_completion_create):
# Set up the mock to raise ConnectionError on the first call mock_client = MagicMock()
# and return None on the second call
mock_chat_completion_create.side_effect = [ # Set up the mock to raise
requests.exceptions.ConnectionError("Connection error"), mock_client.chat.completions.create.side_effect = [
httpx.ConnectError("Connection error"),
None, None,
] ]
# Call the send_with_retries method # Call the send_with_retries method
send_with_retries("model", ["message"], None, False) send_with_retries(mock_client, "model", ["message"], None, False)
# Assert that print was called once
mock_print.assert_called_once() mock_print.assert_called_once()

View file

@ -32,7 +32,7 @@ class TestWholeFileCoder(unittest.TestCase):
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[])
coder.partial_response_content = ( coder.partial_response_content = (
'To print "Hello, World!" in most programming languages, you can use the following' 'To print "Hello, World!" in most programming languages, you can use the following'
' code:\n\n```python\nprint("Hello, World!")\n```\n\nThis code will output "Hello,' ' code:\n\n```python\nprint("Hello, World!")\n```\n\nThis code will output "Hello,'
@ -44,7 +44,7 @@ class TestWholeFileCoder(unittest.TestCase):
def test_no_files_new_file_should_ask(self): def test_no_files_new_file_should_ask(self):
io = InputOutput(yes=False) # <- yes=FALSE io = InputOutput(yes=False) # <- yes=FALSE
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[])
coder.partial_response_content = ( coder.partial_response_content = (
'To print "Hello, World!" in most programming languages, you can use the following' 'To print "Hello, World!" in most programming languages, you can use the following'
' code:\n\nfoo.js\n```python\nprint("Hello, World!")\n```\n\nThis code will output' ' code:\n\nfoo.js\n```python\nprint("Hello, World!")\n```\n\nThis code will output'
@ -61,7 +61,7 @@ class TestWholeFileCoder(unittest.TestCase):
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[sample_file]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[sample_file])
# Set the partial response content with the updated content # Set the partial response content with the updated content
coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```" coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```"
@ -85,7 +85,7 @@ class TestWholeFileCoder(unittest.TestCase):
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[sample_file]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[sample_file])
# Set the partial response content with the updated content # Set the partial response content with the updated content
coder.partial_response_content = f"{sample_file}\n```\n0\n\1\n2\n" coder.partial_response_content = f"{sample_file}\n```\n0\n\1\n2\n"
@ -109,7 +109,7 @@ Quote!
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[sample_file]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[sample_file])
coder.choose_fence() coder.choose_fence()
@ -139,7 +139,7 @@ Quote!
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[sample_file]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[sample_file])
# Set the partial response content with the updated content # Set the partial response content with the updated content
# With path/to/ prepended onto the filename # With path/to/ prepended onto the filename
@ -164,7 +164,7 @@ Quote!
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io) coder = WholeFileCoder(None, main_model=models.GPT35, io=io)
# Set the partial response content with the updated content # Set the partial response content with the updated content
coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```" coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```"
@ -192,7 +192,7 @@ Quote!
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[sample_file]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[sample_file])
# Set the partial response content with the updated content # Set the partial response content with the updated content
coder.partial_response_content = ( coder.partial_response_content = (
@ -235,7 +235,7 @@ after b
""" """
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[fname_a, fname_b]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[fname_a, fname_b])
# Set the partial response content with the updated content # Set the partial response content with the updated content
coder.partial_response_content = response coder.partial_response_content = response
@ -259,7 +259,7 @@ after b
# Initialize WholeFileCoder with the temporary directory # Initialize WholeFileCoder with the temporary directory
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = WholeFileCoder(main_model=models.GPT35, io=io, fnames=[sample_file]) coder = WholeFileCoder(None, main_model=models.GPT35, io=io, fnames=[sample_file])
# Set the partial response content with the updated content # Set the partial response content with the updated content
coder.partial_response_content = ( coder.partial_response_content = (