Skip to content

jremb/oairs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Oairs

A Rust library for working with the OpenAI API.

Endpoints not currently covered:

Currently only tested on Windows.

Use

The main point of entry for working with the API is the Client struct. The typical initialization is intended to be as follows:

// Argument should be &str or String of your API key
let client = Client::new(...);

This returns Client<Keyed> which you can use to access each resource or endpoint. Any parameters that are required by the API will be required arguments to the method:

let model = CompletionModel::TextDavinci003;
client.completion(model);

If the endpoint has optional parameters, they are set by chaining methods.

client.completion(model)
    .prompt("Hello, world")
    .temperature(temp)
    .stream(true)
    // ...

Every request is only executed upon awaiting send():

// ...
let model = ChatModel::Gpt40314;
let mut msgs = Messages::new();

use Msg::System;
use Msg::User;
let system = System("You are a helpful assistant.".to_string());
let user = User("Hello, world.".to_string());

msgs.push(system);
msgs.push(user);

client.chat_completion(model, msgs)
    .send()
    .await
// ...

The return type of every executed request will be Result<reqwest::Response, OairsError>. Every Response can be deserialized by a struct. (As for why I've chosen to return the Response rather than a deserialized struct directly, see the excurses below.)

// ...
let model = EmbeddingModel::TextEmbeddingAda002;
let inputs = vec!["This is a test.".to_string(), "This is another test.".to_string()];

let mut embedding_response = match client.create_embeddings(model, &inputs).send().await {
  Ok(response) => response.json::<Embedding>().await.unwrap(),
  Err(e) => panic!("{e}"),
};

Each method's docstring contains information and examples on deserialization.

Every "deserializable" struct for a reqwest::Response can be saved to JSON via its save_json(...) method.

let model = EditModel::CodeDavinci002;
let model_obj = match client.retrieve_model(&model).send().await {
  Ok(response) => response.json::<ModelObject>().await.unwrap(),
  Err(e) => panic!("{e}"),
};

// The `.json` extension will be appended automatically if not already present
// The method attempts to create any parent directories if they are not found
// to exist. Certain characters that present problems on Windows filenames (e.g., `:`)
// will return an error.
match model_obj.save_json("some/filename") {
    Ok(_) => (),
    Err(e) => eprintln!("Error saving file: {}", e),
}
// ...

The OairsError also has this method available:

// ...
    Err(e) => e.save_json("errors/some_filename.json"),
// ...

At some point I may publish this crate, in which case you can refer to the documentation for more details. In the meantime, much of that documentation already exists in the docstrings.

Credit

Naturally every dependency listed in the Cargo.toml file deserves credit. Special acknowledgement should be given to the tiktoken and tiktoken-rs crates from which I heavily copied (and slightly modified) for the tokenizer module (see the module comments for more information).

Other crates I relied heavily upon: reqwest, serde, and serde_json.

And the following crates which made adding certain things much easier: nom, polars

Excurses

This was a personal project that I've used to try and learn Rust. I haven't focused on speed, but making the library easy to use. Nor do I have the general experience or specific Rust knowledge to know much about optimization. Thus, it may (or may not) be slower than other similar libraries. Along the lines of ease-of-use, I've tried to provide a lot of convenience methods (and plan to add more). Thus, I haven't taken much time to ask myself "Should this be left to the user?" or stuck only to creating an interface for the endpoints. And so it may (or may not) be larger than other similar libraries. Additionally, Rust being a new language for me, it will almost certainly fail to be idiomatic in places. And finally, due to the compounding factors of it being a project in its early stages and my ignorance, the updates will probably often involve breaking changes.

On returning reqwest::Response

Initially I made the return type of every request the appropriate struct from a deserialized response. To make it easier to debug issues and get a better grasp of which fields I could expect from a response (which wasn't always obvious from the API documentation) I switched over to always returning the reqwest::Response. An error would only be returned in the case that reqwest itself failed. I ultimately decided to stick with this basic idea because it allows the macros handling the various send methods to be somewhat simplified and also gives users the option to have more information about the response if they want it (e.g., checking headers for rate-limits).

The only slight reversion to the earlier iteration (in this regard) is that I've screened the status of the responses ahead of time, so that if the Result type is Ok, the user can be assured that the response status is too (i.e., response.status().is_success() should always return true on that branch) and skip straight for deserialization. (However, given the early stage of the project, I may have left some gaps.) IMO, this gives a good balance between giving the user some more control over the response data while also not requiring them to add a lot of lines of code if they don't care about that.

If a user wants, they can still check the headers and confirm the status:

// ...
let response = match client.list_fine_tunes().send().await {
    Ok(r) => r,
    Err(e) => panic!("{e}"),
};

if response.status().is_success() {
    let ft_list = response.json::<FineTunesList>.await.unwrap();
    // or
    let s = response.text().await.unwrap();
    // or ...
} else {
    // handle bad status
}

Releases

No releases published

Packages

No packages published

Languages