-
-
Notifications
You must be signed in to change notification settings - Fork 352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upgrade to run against OpenAI >= 1.0 #400
Conversation
Looking at this diff generated by diff --git a/llm/default_plugins/openai_models.py b/llm/default_plugins/openai_models.py
index 611340d..d787c7a 100644
--- a/llm/default_plugins/openai_models.py
+++ b/llm/default_plugins/openai_models.py
@@ -4,6 +4,9 @@ from llm.utils import dicts_to_table_string
import click
import datetime
import openai
+from openai import OpenAI
+
+client = OpenAI()
import os
try:
@@ -22,7 +25,7 @@ if os.environ.get("LLM_OPENAI_SHOW_RESPONSES"):
click.echo(response.text, err=True)
return response
- openai.requestssession = requests.Session()
+ raise Exception("The 'openai.requestssession' option isn't read in the client API. You will need to pass it when you instantiate the client, e.g. 'OpenAI(requestssession=requests.Session())'")
openai.requestssession.hooks["response"].append(log_response)
@@ -111,7 +114,7 @@ class OpenAIEmbeddingModel(EmbeddingModel):
}
if self.dimensions:
kwargs["dimensions"] = self.dimensions
- results = openai.Embedding.create(**kwargs)["data"]
+ results = client.embeddings.create(**kwargs)["data"]
return ([float(r) for r in result["embedding"]] for result in results)
@@ -305,12 +308,10 @@ class Chat(Model):
response._prompt_json = {"messages": messages}
kwargs = self.build_kwargs(prompt)
if stream:
- completion = openai.ChatCompletion.create(
- model=self.model_name or self.model_id,
- messages=messages,
- stream=True,
- **kwargs,
- )
+ completion = client.chat.completions.create(model=self.model_name or self.model_id,
+ messages=messages,
+ stream=True,
+ **kwargs)
chunks = []
for chunk in completion:
chunks.append(chunk)
@@ -319,12 +320,10 @@ class Chat(Model):
yield content
response.response_json = combine_chunks(chunks)
else:
- completion = openai.ChatCompletion.create(
- model=self.model_name or self.model_id,
- messages=messages,
- stream=False,
- **kwargs,
- )
+ completion = client.chat.completions.create(model=self.model_name or self.model_id,
+ messages=messages,
+ stream=False,
+ **kwargs)
response.response_json = completion.to_dict_recursive()
yield completion.choices[0].message.content
@@ -384,12 +383,10 @@ class Completion(Chat):
response._prompt_json = {"messages": messages}
kwargs = self.build_kwargs(prompt)
if stream:
- completion = openai.Completion.create(
- model=self.model_name or self.model_id,
- prompt="\n".join(messages),
- stream=True,
- **kwargs,
- )
+ completion = client.completions.create(model=self.model_name or self.model_id,
+ prompt="\n".join(messages),
+ stream=True,
+ **kwargs)
chunks = []
for chunk in completion:
chunks.append(chunk)
@@ -398,12 +395,10 @@ class Completion(Chat):
yield content
response.response_json = combine_chunks(chunks)
else:
- completion = openai.Completion.create(
- model=self.model_name or self.model_id,
- prompt="\n".join(messages),
- stream=False,
- **kwargs,
- )
+ completion = client.completions.create(model=self.model_name or self.model_id,
+ prompt="\n".join(messages),
+ stream=False,
+ **kwargs)
response.response_json = completion.to_dict_recursive()
yield completion.choices[0]["text"] It basically all comes down to these differences: openai.Embedding.create(...)
client.embeddings.create(...)
openai.ChatCompletion.create(...)
client.chat.completions.create(...)
openai.Completion.create(...)
client.completions.create(...) So... I could add a |
I'm going to do this: client = get_openai_client(...)
client.ChatCompletion.create(...)
client.Completion.create(...)
client.Embedding.create(...) Where my special |
On the latest version: >>> openai.version.VERSION
'1.10.0'
>>> openai.version.VERSION.split('.')[0]
'1' And on the pre-1.0 version: >>> import openai
>>> openai.version.VERSION
'0.28.1'
>>> openai.version.VERSION.split('.')[0]
'0' I can use that to detect the old version. |
llm 'hello world'
I'm going to have to get my |
Oh this is annoying. I've been passing llm/llm/default_plugins/openai_models.py Lines 331 to 355 in 469644c
But it looks like I need to pass those to that new constructor instead: client=OpenAI(
api_key="<key>",
base_url="<base_url>",
http_client=httpx.Client(
headers={
"header": "<key>",
...
}
)
) This is going to REALLY mess up my compatibility shim. |
I think I have two sensible options:
I like option 2 a lot - it's much nicer for me to write code against the new API and have a compatibility shim, since I can then easily drop the shim later on if I change my mind. I'm going to spike on 2 for a bit and, if I can't get that working, switch to option 1 and drop <1.0 entirely. |
Urgh:
From the stack trace:
|
Debugger exploration:
|
I'm going to give up on the |
Well here's a painful thing...
It turns out the new OpenAI library uses Pydantic. I've had huge pain keeping LLM compatible with Pydantic 1 and Pydantic 2 already! But... https://github.com/openai/openai-python/blob/0c1e58d511bd60c4dd47ea8a8c0820dc2d013d1d/pyproject.toml#L12 says dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=1.9.0, <3", So maybe OpenAI is compatible with both Pydantic versions itself? Yes, it turns out there are! Here's their own Pydantic 1 v. 2 compatibility shim: https://github.com/openai/openai-python/blob/0c1e58d511bd60c4dd47ea8a8c0820dc2d013d1d/src/openai/_compat.py#L20 |
From openai/openai-python@v0.28.1...v1.0.0 I confirmed that pydantic was indeed one of the new things they added in 1.0. |
Now my tests are failing and I'm worried it might be because my mocks don't work any more:
Did 1.0 change the HTTP library they use? Yes - it looks like they switched from |
openai/openai-python#742 confirms that llm/llm/default_plugins/openai_models.py Lines 20 to 26 in 53c845e
|
So maybe I can ditch |
Here's what that does:
That's good enough, I'll make that change. |
It's not as good - you don't get the full response, which is the thing that was most useful. But maybe I'll add that back in again in the future. |
Got this test failure:
The debugger shows:
|
The problem here is that I used to store the exact JSON that came back from the API - but the new Pydantic layer in OpenAI 1.0 reshapes that original information into something a lot more verbose, with a ton of |
Pushing what I have so far. |
I'm going to clean that up with this: def remove_dict_none_values(d: dict) -> dict:
"""
Recursively remove keys with value of None or value of a dict that is all values of None
"""
if not isinstance(d, dict):
return d
new_dict = {}
for key, value in d.items():
if value is not None:
if isinstance(value, dict):
nested = remove_dict_none_values(value)
if nested:
new_dict[key] = nested
elif isinstance(value, list):
new_dict[key] = [remove_dict_none_values(v) for v in value]
else:
new_dict[key] = value
return new_dict Written with help of: https://chat.openai.com/share/0dbf00c3-3feb-423c-98aa-7a4cca89023c |
One last error:
From this test: Lines 460 to 496 in 469644c
openai/openai-python#742 says:
|
Well they passed on my laptop! Failing here because I forgot to run linters. |
I used ChatGPT to help port the requests mock tests to pytest: https://chat.openai.com/share/1ea97304-1ceb-4e4c-9213-bae9949cd261 |
Turns out Black was failing because I needed to upgrade Black in my dev environment (as of only 45 minutes ago: https://github.com/psf/black/releases/tag/24.1.0)
diff --git a/llm/cli.py b/llm/cli.py
index 3fa2ecc..03a6b09 100644
--- a/llm/cli.py
+++ b/llm/cli.py
@@ -746,12 +746,16 @@ def logs_list(
click.echo(
"# {}{}\n{}".format(
row["datetime_utc"].split(".")[0],
- " conversation: {}".format(row["conversation_id"])
- if should_show_conversation
- else "",
- "\nModel: **{}**\n".format(row["model"])
- if should_show_conversation
- else "",
+ (
+ " conversation: {}".format(row["conversation_id"])
+ if should_show_conversation
+ else ""
+ ),
+ (
+ "\nModel: **{}**\n".format(row["model"])
+ if should_show_conversation
+ else ""
+ ),
)
)
# In conversation log mode only show it for the first one
diff --git a/llm/embeddings.py b/llm/embeddings.py
index 8c5333f..5efeda0 100644
--- a/llm/embeddings.py
+++ b/llm/embeddings.py
@@ -220,12 +220,12 @@ class Collection:
"collection_id": collection_id,
"id": id,
"embedding": llm.encode(embedding),
- "content": value
- if (store and isinstance(value, str))
- else None,
- "content_blob": value
- if (store and isinstance(value, bytes))
- else None,
+ "content": (
+ value if (store and isinstance(value, str)) else None
+ ),
+ "content_blob": (
+ value if (store and isinstance(value, bytes)) else None
+ ),
"content_hash": self.content_hash(value),
"metadata": json.dumps(metadata) if metadata else None,
"updated": int(time.time()), That's a nice cosmetic improvement. |
Heya @simonw, I'm one of the maintainers of the Few follow-ups:
This is good feedback, we may add that in the future. In the meantime, you can use httpx's event_hooks to achieve this if you like.
This is good feedback – we've been torn between sticking with Pydantic defaults (what you see here, lots of Note that I believe that your EDIT: I should ask, do you have any other feedback, whether overall / high-level, or nitty-gritty, about how the python SDK could be better or how the experience porting from the v0 could be better? |
Refs:
Original goal was "Upgrade to run against both OpenAI < 1.0 and OpenAI >= 1.0" but I eventually determined that the changes between the two were just too extensive for it to be worth trying to build a compatibility shim.