mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-02 18:54:59 +00:00
merge in openai upgrade
This commit is contained in:
commit
fe9423d7b8
23 changed files with 304 additions and 201 deletions
|
@ -42,6 +42,7 @@ def wrap_fence(name):
|
|||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
|
||||
|
||||
class Coder:
|
||||
client = None
|
||||
abs_fnames = None
|
||||
repo = None
|
||||
last_aider_commit_hash = None
|
||||
|
@ -58,6 +59,7 @@ class Coder:
|
|||
main_model=None,
|
||||
edit_format=None,
|
||||
io=None,
|
||||
client=None,
|
||||
skip_model_availabily_check=False,
|
||||
**kwargs,
|
||||
):
|
||||
|
@ -67,26 +69,28 @@ class Coder:
|
|||
main_model = models.GPT4
|
||||
|
||||
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:
|
||||
io.tool_error(
|
||||
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:
|
||||
edit_format = main_model.edit_format
|
||||
|
||||
if edit_format == "diff":
|
||||
return EditBlockCoder(main_model, io, **kwargs)
|
||||
return EditBlockCoder(client, main_model, io, **kwargs)
|
||||
elif edit_format == "whole":
|
||||
return WholeFileCoder(main_model, io, **kwargs)
|
||||
return WholeFileCoder(client, main_model, io, **kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown edit format {edit_format}")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client,
|
||||
main_model,
|
||||
io,
|
||||
fnames=None,
|
||||
|
@ -105,6 +109,8 @@ class Coder:
|
|||
voice_language=None,
|
||||
aider_ignore_file=None,
|
||||
):
|
||||
self.client = client
|
||||
|
||||
if not fnames:
|
||||
fnames = []
|
||||
|
||||
|
@ -161,7 +167,9 @@ class Coder:
|
|||
|
||||
if use_git:
|
||||
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
|
||||
except FileNotFoundError:
|
||||
self.repo = None
|
||||
|
@ -192,6 +200,7 @@ class Coder:
|
|||
self.io.tool_output(f"Added {fname} to the chat.")
|
||||
|
||||
self.summarizer = ChatSummary(
|
||||
self.client,
|
||||
models.Model.weak_model(),
|
||||
self.main_model.max_chat_history_tokens,
|
||||
)
|
||||
|
@ -305,6 +314,13 @@ class Coder:
|
|||
|
||||
def get_files_messages(self):
|
||||
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:
|
||||
files_content = self.gpt_prompts.files_content_prefix
|
||||
files_content += self.get_files_content()
|
||||
|
@ -313,12 +329,6 @@ class Coder:
|
|||
|
||||
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 = [
|
||||
dict(role="user", content=all_content),
|
||||
dict(role="assistant", content="Ok."),
|
||||
|
@ -500,7 +510,7 @@ class Coder:
|
|||
interrupted = self.send(messages, functions=self.functions)
|
||||
except ExhaustedContextWindow:
|
||||
exhausted = True
|
||||
except openai.error.InvalidRequestError as err:
|
||||
except openai.BadRequestError as err:
|
||||
if "maximum context length" in str(err):
|
||||
exhausted = True
|
||||
else:
|
||||
|
@ -617,7 +627,9 @@ class Coder:
|
|||
|
||||
interrupted = False
|
||||
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())
|
||||
|
||||
if self.stream:
|
||||
|
@ -971,9 +983,16 @@ class Coder:
|
|||
return True
|
||||
|
||||
|
||||
def check_model_availability(io, main_model):
|
||||
available_models = openai.Model.list()
|
||||
model_ids = sorted(model.id for model in available_models["data"])
|
||||
def check_model_availability(io, client, main_model):
|
||||
try:
|
||||
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:
|
||||
return True
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
||||
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*.
|
||||
To make a file *read-write*, ask the user to *add it to the chat*.
|
||||
"""
|
||||
|
|
|
@ -471,7 +471,7 @@ class Commands:
|
|||
|
||||
if not self.voice:
|
||||
try:
|
||||
self.voice = voice.Voice()
|
||||
self.voice = voice.Voice(self.coder.client)
|
||||
except voice.SoundDeviceError:
|
||||
self.io.tool_error(
|
||||
"Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?"
|
||||
|
|
|
@ -7,7 +7,8 @@ from aider.sendchat import simple_send_with_retries
|
|||
|
||||
|
||||
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.max_tokens = max_tokens
|
||||
self.model = model
|
||||
|
@ -84,7 +85,7 @@ class ChatSummary:
|
|||
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:
|
||||
raise ValueError(f"summarizer unexpectedly failed for {self.model.name}")
|
||||
summary = prompts.summary_prefix + summary
|
||||
|
|
|
@ -157,12 +157,13 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
default=False,
|
||||
help="Override to skip model availability check (default: False)",
|
||||
)
|
||||
default_3_model = models.GPT35_1106
|
||||
core_group.add_argument(
|
||||
"-3",
|
||||
action="store_const",
|
||||
dest="model",
|
||||
const=models.GPT35_16k.name,
|
||||
help=f"Use {models.GPT35_16k.name} model for the main chat (gpt-4 is better)",
|
||||
const=default_3_model.name,
|
||||
help=f"Use {default_3_model.name} model for the main chat (gpt-4 is better)",
|
||||
)
|
||||
core_group.add_argument(
|
||||
"--voice-language",
|
||||
|
@ -176,27 +177,22 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
model_group.add_argument(
|
||||
"--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(
|
||||
"--openai-api-type",
|
||||
metavar="OPENAI_API_TYPE",
|
||||
help="Specify the openai.api_type",
|
||||
help="Specify the api_type",
|
||||
)
|
||||
model_group.add_argument(
|
||||
"--openai-api-version",
|
||||
metavar="OPENAI_API_VERSION",
|
||||
help="Specify the openai.api_version",
|
||||
help="Specify the api_version",
|
||||
)
|
||||
model_group.add_argument(
|
||||
"--openai-api-deployment-id",
|
||||
metavar="OPENAI_API_DEPLOYMENT_ID",
|
||||
help="Specify the deployment_id arg to be passed to openai.ChatCompletion.create()",
|
||||
)
|
||||
model_group.add_argument(
|
||||
"--openai-api-engine",
|
||||
metavar="OPENAI_API_ENGINE",
|
||||
help="Specify the engine arg to be passed to openai.ChatCompletion.create()",
|
||||
help="Specify the deployment_id",
|
||||
)
|
||||
model_group.add_argument(
|
||||
"--edit-format",
|
||||
|
@ -380,6 +376,12 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
metavar="COMMAND",
|
||||
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(
|
||||
"--encoding",
|
||||
default="utf-8",
|
||||
|
@ -492,23 +494,34 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
)
|
||||
return 1
|
||||
|
||||
openai.api_key = args.openai_api_key
|
||||
for attr in ("base", "type", "version", "deployment_id", "engine"):
|
||||
arg_key = f"openai_api_{attr}"
|
||||
val = getattr(args, arg_key)
|
||||
if val is not None:
|
||||
mod_key = f"api_{attr}"
|
||||
setattr(openai, mod_key, val)
|
||||
io.tool_output(f"Setting openai.{mod_key}={val}")
|
||||
if args.openai_api_type == "azure":
|
||||
client = openai.AzureOpenAI(
|
||||
api_key=args.openai_api_key,
|
||||
azure_endpoint=args.openai_api_base,
|
||||
api_version=args.openai_api_version,
|
||||
azure_deployment=args.openai_api_deployment_id,
|
||||
)
|
||||
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:
|
||||
coder = Coder.create(
|
||||
main_model,
|
||||
args.edit_format,
|
||||
io,
|
||||
args.skip_model_availability_check,
|
||||
main_model=main_model,
|
||||
edit_format=args.edit_format,
|
||||
io=io,
|
||||
skip_model_availabily_check=args.skip_model_availability_check,
|
||||
client=client,
|
||||
##
|
||||
fnames=fnames,
|
||||
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}")
|
||||
|
||||
if args.message:
|
||||
io.add_to_input_history(args.message)
|
||||
io.tool_output()
|
||||
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:
|
||||
coder.run()
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from .openrouter import OpenRouterModel
|
|||
|
||||
GPT4 = Model.create("gpt-4")
|
||||
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")
|
||||
|
||||
__all__ = [
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import json
|
||||
import math
|
||||
|
||||
import openai
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class Model:
|
||||
name = None
|
||||
edit_format = None
|
||||
|
@ -20,12 +18,12 @@ class Model:
|
|||
completion_price = None
|
||||
|
||||
@classmethod
|
||||
def create(cls, name):
|
||||
def create(cls, name, client=None):
|
||||
from .openai import OpenAIModel
|
||||
from .openrouter import OpenRouterModel
|
||||
|
||||
if "openrouter.ai" in openai.api_base:
|
||||
return OpenRouterModel(name)
|
||||
if client and client.base_url.host == "openrouter.ai":
|
||||
return OpenRouterModel(client, name)
|
||||
return OpenAIModel(name)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -37,11 +35,11 @@ class Model:
|
|||
|
||||
@staticmethod
|
||||
def weak_model():
|
||||
return Model.create("gpt-3.5-turbo")
|
||||
return Model.create("gpt-3.5-turbo-1106")
|
||||
|
||||
@staticmethod
|
||||
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):
|
||||
if not self.tokenizer:
|
||||
|
@ -61,8 +59,6 @@ class Model:
|
|||
:param fname: The filename of 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)
|
||||
|
||||
# If the image is larger than 2048 in any dimension, scale it down to fit within 2048x2048
|
||||
|
|
|
@ -55,6 +55,7 @@ class OpenAIModel(Model):
|
|||
if self.is_gpt35():
|
||||
self.edit_format = "whole"
|
||||
self.always_available = True
|
||||
self.send_undo_reply = False
|
||||
|
||||
if self.name == "gpt-3.5-turbo-1106":
|
||||
self.prompt_price = 0.001
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import openai
|
||||
import tiktoken
|
||||
|
||||
from .model import Model
|
||||
|
@ -7,13 +6,9 @@ cached_model_details = None
|
|||
|
||||
|
||||
class OpenRouterModel(Model):
|
||||
def __init__(self, name):
|
||||
if name == "gpt-4":
|
||||
name = "openai/gpt-4"
|
||||
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"
|
||||
def __init__(self, client, name):
|
||||
if name.startswith("gpt-4") or name.startswith("gpt-3.5-turbo"):
|
||||
name = "openai/" + name
|
||||
|
||||
self.name = name
|
||||
self.edit_format = edit_format_for_model(name)
|
||||
|
@ -24,7 +19,7 @@ class OpenRouterModel(Model):
|
|||
|
||||
global cached_model_details
|
||||
if cached_model_details is None:
|
||||
cached_model_details = openai.Model.list().data
|
||||
cached_model_details = client.models.list().data
|
||||
found = next(
|
||||
(details for details in cached_model_details if details.get("id") == name), None
|
||||
)
|
||||
|
|
|
@ -16,7 +16,8 @@ class GitRepo:
|
|||
aider_ignore_spec = None
|
||||
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
|
||||
|
||||
if git_dname:
|
||||
|
@ -102,9 +103,7 @@ class GitRepo:
|
|||
|
||||
def get_commit_message(self, diffs, context):
|
||||
if len(diffs) >= 4 * 1024 * 4:
|
||||
self.io.tool_error(
|
||||
f"Diff is too large for {models.GPT35.name} to generate a commit message."
|
||||
)
|
||||
self.io.tool_error("Diff is too large to generate a commit message.")
|
||||
return
|
||||
|
||||
diffs = "# Diffs:\n" + diffs
|
||||
|
@ -120,7 +119,7 @@ class GitRepo:
|
|||
]
|
||||
|
||||
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:
|
||||
break
|
||||
|
||||
|
|
|
@ -2,17 +2,13 @@ import hashlib
|
|||
import json
|
||||
|
||||
import backoff
|
||||
import httpx
|
||||
import openai
|
||||
import requests
|
||||
|
||||
# from diskcache import Cache
|
||||
from openai.error import (
|
||||
APIConnectionError,
|
||||
APIError,
|
||||
RateLimitError,
|
||||
ServiceUnavailableError,
|
||||
Timeout,
|
||||
)
|
||||
from openai import APIConnectionError, InternalServerError, RateLimitError
|
||||
|
||||
from aider.dump import dump # noqa: F401
|
||||
|
||||
CACHE_PATH = "~/.aider.send.cache.v1"
|
||||
CACHE = None
|
||||
|
@ -22,19 +18,20 @@ CACHE = None
|
|||
@backoff.on_exception(
|
||||
backoff.expo,
|
||||
(
|
||||
Timeout,
|
||||
APIError,
|
||||
ServiceUnavailableError,
|
||||
InternalServerError,
|
||||
RateLimitError,
|
||||
APIConnectionError,
|
||||
requests.exceptions.ConnectionError,
|
||||
httpx.ConnectError,
|
||||
),
|
||||
max_tries=10,
|
||||
on_backoff=lambda details: print(
|
||||
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(
|
||||
model=model_name,
|
||||
messages=messages,
|
||||
|
@ -44,17 +41,8 @@ def send_with_retries(model_name, messages, functions, stream):
|
|||
if functions is not None:
|
||||
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
|
||||
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):
|
||||
kwargs['model'] = "gpt-4-vision-preview"
|
||||
# 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:
|
||||
return hash_object, CACHE[key]
|
||||
|
||||
res = openai.ChatCompletion.create(**kwargs)
|
||||
res = client.chat.completions.create(**kwargs)
|
||||
|
||||
if not stream and CACHE is not None:
|
||||
CACHE[key] = res
|
||||
|
@ -76,14 +64,15 @@ def send_with_retries(model_name, messages, functions, stream):
|
|||
return hash_object, res
|
||||
|
||||
|
||||
def simple_send_with_retries(model_name, messages):
|
||||
def simple_send_with_retries(client, model_name, messages):
|
||||
try:
|
||||
_hash, response = send_with_retries(
|
||||
client=client,
|
||||
model_name=model_name,
|
||||
messages=messages,
|
||||
functions=None,
|
||||
stream=False,
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
except (AttributeError, openai.error.InvalidRequestError):
|
||||
except (AttributeError, openai.BadRequestError):
|
||||
return
|
||||
|
|
|
@ -4,7 +4,6 @@ import tempfile
|
|||
import time
|
||||
|
||||
import numpy as np
|
||||
import openai
|
||||
|
||||
try:
|
||||
import soundfile as sf
|
||||
|
@ -27,7 +26,7 @@ class Voice:
|
|||
|
||||
threshold = 0.15
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, client):
|
||||
if sf is None:
|
||||
raise SoundDeviceError
|
||||
try:
|
||||
|
@ -38,6 +37,8 @@ class Voice:
|
|||
except (OSError, ModuleNotFoundError):
|
||||
raise SoundDeviceError
|
||||
|
||||
self.client = client
|
||||
|
||||
def callback(self, indata, frames, time, status):
|
||||
"""This is called (from a separate thread) for each audio block."""
|
||||
rms = np.sqrt(np.mean(indata**2))
|
||||
|
@ -88,9 +89,11 @@ class Voice:
|
|||
file.write(self.q.get())
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue