Skip to content
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

Add async support #146

Merged
merged 11 commits into from
Jan 5, 2023
Merged

Add async support #146

merged 11 commits into from
Jan 5, 2023

Conversation

Andrew-Chen-Wang
Copy link
Contributor

@Andrew-Chen-Wang Andrew-Chen-Wang commented Dec 6, 2022

Fixes #98

Adds asyncio support for API. Does not support CLI (does not make sense).

I haven't tested file upload. aiohttp does not include the files parameter like requests, so I'm using requests' private utility function in the ApiRequestor

Usage (notice the a prefix, used in CPython lib, Django, and other libs):

import openai

openai.api_key = "sk-..."

async def main():
    await openai.Completion.acreate(prompt="This is a test", engine="text-ada-001")

Installation of aiohttp can include speedups that aren't included in setup.py. aiohttp is a required package, but it doesn't have to be. We'd just need to remove the typing.

@Andrew-Chen-Wang Andrew-Chen-Wang changed the title [WIP] Add async support Add async support Dec 6, 2022
@Andrew-Chen-Wang Andrew-Chen-Wang marked this pull request as ready for review December 6, 2022 07:40
@Andrew-Chen-Wang Andrew-Chen-Wang mentioned this pull request Dec 6, 2022
borisdayma pushed a commit to borisdayma/openai-python that referenced this pull request Dec 7, 2022
* overload output type depending on stream literal (openai#142)

* Bump to v22

* [numpy] change version (openai#143)

* [numpy] change version

* update comments

* no version for numpy

* Fix timeouts (openai#137)

* Fix timeouts

* Rename to request_timeout and add to readme

* Dev/hallacy/request timeout takes tuples (openai#144)

* Add tuple typing for request_timeout

* imports

* [api_requestor] Log request_id with response (openai#145)

* Only import wandb as needed (openai#146)

Co-authored-by: Felipe Petroski Such <felipe@openai.com>
Co-authored-by: Henrique Oliveira Pinto <hponde@openai.com>
Co-authored-by: Rachel Lim <rachel@openai.com>
@MaxTretikov
Copy link

Love this addition, hope it gets merged. Wanted to add that I seem to have been getting Unclosed client session errors when using this:

client_session: <aiohttp.client.ClientSession object at 0x104306b00>
Unclosed client session

* This is due to a lack of an async __del__ method
@Andrew-Chen-Wang
Copy link
Contributor Author

Thanks for catching that @MaxTretikov ! This should be resolved in the latest commit

Copy link
Contributor

@ddeville ddeville left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much @Andrew-Chen-Wang, this was quite the effort!

I've left a few comments, mostly on the APIRequestor since it's the most interesting part imo.

An obvious issue when looking at this patch is that it introduces a ton of duplication but that's usually what happens when wanting to provide both a sync and async library without requiring callers to use async_to_sync or the like. Another option I've seen others take is to offer different classes entirely for the aio version of the library so that the current sync ones don't need to change but we'd basically end up with even more duplication...

Maybe there's a future world where this library does become entirely async (it's mostly IO bound anyway) and callers who want sync then need to run the task to completion in an event loop on a thread of their own. I'm not sure, but for now I personally feel like this approach is totally fine.

openai/api_requestor.py Outdated Show resolved Hide resolved
openai/api_requestor.py Show resolved Hide resolved
Comment on lines 462 to 464
data, content_type = requests.models.RequestEncodingMixin._encode_files(
files, data
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aiohttp has MultipartWriter for this use case. Not as trivial as just passing a dictionary of files as with requests but I think that'd be the proper way to do this.

Copy link
Contributor Author

@Andrew-Chen-Wang Andrew-Chen-Wang Dec 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the preparation of the data will be fairly different between requests and aiohttp causing more duplication of code. I'm planning of essentially copying _encode_files(albeit the actual appending of files is different). Is that alright?

Also, feel free to add my fork as a remote and commit freely. I've enabled maintainers to be able to commit to my fork.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I took a quick look at MultipartWriter and the implementation of _encode_files and while it should work for us here that's a fair amount of somewhat brittle code that could be easy to mess up. Maybe adding a comment/TODO above this line explaining what is going on is good enough for now.

Comment on lines 51 to 53
aiosession: ContextVar[Optional["ClientSession"]] = ContextVar(
"aiohttp-session", default=None
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I thought this was just a global but after learning more about Context and ContextVar I realized that it enabled the thing I was scared this would prevent: using different sessions, by making a copy of the current context, setting the new session and running the code through the new context, thus having the openai module picking the new context without impacting other code that will still get the other one.

This might be worth putting as an example in the readme since it was not clear to me that this was possible without first reading more about ContextVar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already addressed in the README I believe in the async section. Please add suggestions. I've also added a comment in the source code itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably fine I agree. I think it's mostly a matter of knowing how ContextVar works which I expect people dealing to async code to do (and that I didn't initially :))

openai/api_requestor.py Outdated Show resolved Hide resolved
openai/api_requestor.py Outdated Show resolved Hide resolved
openai/api_resources/abstract/engine_api_resource.py Outdated Show resolved Hide resolved
openai/cli.py Outdated Show resolved Hide resolved
@@ -21,9 +21,10 @@
"openpyxl>=3.0.7", # Needed for CLI fine-tuning data preparation tool xlsx format
"numpy",
"typing_extensions", # Needed for type hints for mypy
"aiohttp", # Needed for async support
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option could be to move this to extras_require under an async feature and then we would raise an exception in api_requestor.py if we can't import aiohttp. Although it's also fine to always require it tbh.

Copy link
Contributor Author

@Andrew-Chen-Wang Andrew-Chen-Wang Dec 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair, it would be better, but typing would be a pain for mypy users. In redis-py, we install async-timeout, even if you only need the sync Redis

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah fair enough.

@Andrew-Chen-Wang
Copy link
Contributor Author

Thanks for your time and the review @ddeville !

Copy link
Contributor

@ddeville ddeville left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, there might be a couple of small pieces to update but I think this is a very good (huge) first step.

Let's do this!

@ddeville ddeville requested a review from hallacy January 3, 2023 19:21
@ddeville
Copy link
Contributor

ddeville commented Jan 3, 2023

Would love for @hallacy to take a look before we merge though.

Copy link
Collaborator

@hallacy hallacy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it! Lets merge it

@Andrew-Chen-Wang
Copy link
Contributor Author

Andrew-Chen-Wang commented Jan 4, 2023

thanks @ddeville and @hallacy !!

in the future to possibly prevent further duplication of code, I highly recommend two resources:

  • SansIO approach to implementing a sync/async lib (ex impl)
  • A simpler method can be found in redis-py itself. In this method, we have the commands folder (similar to the api_resources folder) and the redis.Redis and redis.asyncio.Redis class (similar to ApiRequestor). The Redis classes implement their own execute_command method and a command from api_resources runs execute_command. The reason for this PR's amount of duplication of code is because of útil.convert_to_openai_object. If that function was instead moved to ApiRequestor.request, you're left with a mixin that is potentially compatible for both sync and async classes. But it can be tricky depending on future implementation and CLI compatibility

@ddeville
Copy link
Contributor

ddeville commented Jan 4, 2023

Thanks for the links @Andrew-Chen-Wang this is super interesting. From a quick glance, it looks like it works more or less like this:

class BaseExecutor:
    def execute_cmd(...):
        ...

class SyncExecutor(BaseExecutor):
    def execute_cmd(...):
        return get(...)

class AsyncExecutor(BaseExecutor):
    async def execute_cmd(...):  # not sure how typing works here though since they return different types
        return await get(...)

class Model:
    def __init__(self, client: BaseExecutor):
        self.execute_cmd = client.execute_cmd
   
   def something(self, ...):
       request = ...
       return self.execute_cmd(request)

And then you would use it like that

# from a sync context
def main():
    model = Model(SyncExecutor())
    ret = model.something()

# from an async context
async def main():
    model = Model(AsyncExecutor())
    ret = await model.something()

(so basically BaseExecutor is a mixin and here convert_to_openai_object is a problem because it is called in the calling code rather than in ApiRequestor which means that Model would need to be able to await the result if the executor was async rather than just returning the coroutine?)

Am I reading this right?

The only issue I can think of is that the methods on Model are not marked as async but still return coroutines in the async case which is not super clear and might lead to problems where callers forget to await on their result. Has this been a problem in your experience?

@Andrew-Chen-Wang
Copy link
Contributor Author

Andrew-Chen-Wang commented Jan 4, 2023

You got it right!

To answer the question on typing, in redis-py, we make a type Union with Awaitable and result type.

However the reason I make these suggestions is because the example usage you've got it quite unwieldy, hence a preference for the current implementation in this PR. A better implementation would be to have something similar to the Redis class, like ApiRequestor but named OpenAI, that could call commands like OpenAI().Completion(...)

But that's essentially a complete rewrite that I'm. not quite for at the moment. Would prefer to have the PR merged as it seems like a complete rewrite would be necessary (looking at the stripe sdk rn which this lib is forked from; for reference: stripe/stripe-python#715 (comment))

@ddeville ddeville merged commit 0abf641 into openai:main Jan 5, 2023
@ksromero
Copy link

ksromero commented Jan 5, 2023

When is the release of this?

@Andrew-Chen-Wang Andrew-Chen-Wang deleted the async-support branch January 5, 2023 02:54
@@ -84,17 +140,33 @@ def download(

if typed_api_type in (ApiType.AZURE, ApiType.AZURE_AD):
base = cls.class_url()
url = "/%s%s/%s/content?api-version=%s" % (
url = "/%s%s/%s?api-version=%s" % (
cls.azure_api_prefix,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why the "content" part was removed?

cgayapr pushed a commit to cgayapr/openai-python that referenced this pull request Dec 14, 2024
* overload output type depending on stream literal (openai#142)

* Bump to v22

* [numpy] change version (openai#143)

* [numpy] change version

* update comments

* no version for numpy

* Fix timeouts (openai#137)

* Fix timeouts

* Rename to request_timeout and add to readme

* Dev/hallacy/request timeout takes tuples (openai#144)

* Add tuple typing for request_timeout

* imports

* [api_requestor] Log request_id with response (openai#145)

* Only import wandb as needed (openai#146)

Co-authored-by: Felipe Petroski Such <felipe@openai.com>
Co-authored-by: Henrique Oliveira Pinto <hponde@openai.com>
Co-authored-by: Rachel Lim <rachel@openai.com>
cgayapr pushed a commit to cgayapr/openai-python that referenced this pull request Dec 14, 2024
* Add async support

* Fix aiohttp requests
* Fix some syntax errors

* Close aiohttp session properly
* This is due to a lack of an async __del__ method

* Fix code per review

* Fix async tests and some mypy errors

* Run black

* Add todo for multipart form generation

* Fix more mypy

* Fix exception type

* Don't yield twice

Co-authored-by: Damien Deville <damien@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Async requests
6 participants