Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: 64bit/async-openai
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: async-openai-v0.27.0
Choose a base ref
...
head repository: 64bit/async-openai
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: async-openai-v0.27.1
Choose a head ref
  • 3 commits
  • 7 files changed
  • 3 contributors

Commits on Jan 11, 2025

  1. [Example]: Use serde, schemars to make structure output code easy (#301)

    * Add structured-outputs-schemars
    
    * Upadte naming and using crates infor
    cptrodgers authored Jan 11, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    f6792f3 View commit details

Commits on Jan 12, 2025

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    42dda26 View commit details
  2. chore: Release

    64bit committed Jan 12, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    64bit Himanshu Neema
    Copy the full SHA
    12108a0 View commit details
5 changes: 2 additions & 3 deletions async-openai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "async-openai"
version = "0.27.0"
version = "0.27.1"
authors = ["Himanshu Neema"]
categories = ["api-bindings", "web-programming", "asynchronous"]
keywords = ["openai", "async", "openapi", "ai"]
description = "Rust library for OpenAI"
edition = "2021"
rust-version = "1.65"
rust-version = "1.75"
license = "MIT"
readme = "README.md"
homepage = "https://github.com/64bit/async-openai"
@@ -43,7 +43,6 @@ tokio-stream = "0.1.17"
tokio-util = { version = "0.7.13", features = ["codec", "io-util"] }
tracing = "0.1.41"
derive_builder = "0.20.2"
async-convert = "1.0.0"
secrecy = { version = "0.10.3", features = ["serde"] }
bytes = "1.9.0"
eventsource-stream = "0.2.3"
10 changes: 6 additions & 4 deletions async-openai/src/client.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ use std::pin::Pin;

use bytes::Bytes;
use futures::{stream::StreamExt, Stream};
use reqwest::multipart::Form;
use reqwest_eventsource::{Event, EventSource, RequestBuilderExt};
use serde::{de::DeserializeOwned, Serialize};

@@ -11,6 +12,7 @@ use crate::{
file::Files,
image::Images,
moderation::Moderations,
util::AsyncTryFrom,
Assistants, Audio, AuditLogs, Batches, Chat, Completions, Embeddings, FineTuning, Invites,
Models, Projects, Threads, Users, VectorStores,
};
@@ -266,7 +268,7 @@ impl<C: Config> Client<C> {
/// POST a form at {path} and return the response body
pub(crate) async fn post_form_raw<F>(&self, path: &str, form: F) -> Result<Bytes, OpenAIError>
where
reqwest::multipart::Form: async_convert::TryFrom<F, Error = OpenAIError>,
Form: AsyncTryFrom<F, Error = OpenAIError>,
F: Clone,
{
let request_maker = || async {
@@ -275,7 +277,7 @@ impl<C: Config> Client<C> {
.post(self.config.url(path))
.query(&self.config.query())
.headers(self.config.headers())
.multipart(async_convert::TryFrom::try_from(form.clone()).await?)
.multipart(<Form as AsyncTryFrom<F>>::try_from(form.clone()).await?)
.build()?)
};

@@ -286,7 +288,7 @@ impl<C: Config> Client<C> {
pub(crate) async fn post_form<O, F>(&self, path: &str, form: F) -> Result<O, OpenAIError>
where
O: DeserializeOwned,
reqwest::multipart::Form: async_convert::TryFrom<F, Error = OpenAIError>,
Form: AsyncTryFrom<F, Error = OpenAIError>,
F: Clone,
{
let request_maker = || async {
@@ -295,7 +297,7 @@ impl<C: Config> Client<C> {
.post(self.config.url(path))
.query(&self.config.query())
.headers(self.config.headers())
.multipart(async_convert::TryFrom::try_from(form.clone()).await?)
.multipart(<Form as AsyncTryFrom<F>>::try_from(form.clone()).await?)
.build()?)
};

20 changes: 7 additions & 13 deletions async-openai/src/types/impls.rs
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ use crate::{
download::{download_url, save_b64},
error::OpenAIError,
types::InputSource,
util::{create_all_dir, create_file_part},
util::{create_all_dir, create_file_part, AsyncTryFrom},
};

use bytes::Bytes;
@@ -821,8 +821,7 @@ impl Default for ChatCompletionRequestToolMessageContent {

// start: types to multipart from

#[async_convert::async_trait]
impl async_convert::TryFrom<CreateTranscriptionRequest> for reqwest::multipart::Form {
impl AsyncTryFrom<CreateTranscriptionRequest> for reqwest::multipart::Form {
type Error = OpenAIError;

async fn try_from(request: CreateTranscriptionRequest) -> Result<Self, Self::Error> {
@@ -858,8 +857,7 @@ impl async_convert::TryFrom<CreateTranscriptionRequest> for reqwest::multipart::
}
}

#[async_convert::async_trait]
impl async_convert::TryFrom<CreateTranslationRequest> for reqwest::multipart::Form {
impl AsyncTryFrom<CreateTranslationRequest> for reqwest::multipart::Form {
type Error = OpenAIError;

async fn try_from(request: CreateTranslationRequest) -> Result<Self, Self::Error> {
@@ -884,8 +882,7 @@ impl async_convert::TryFrom<CreateTranslationRequest> for reqwest::multipart::Fo
}
}

#[async_convert::async_trait]
impl async_convert::TryFrom<CreateImageEditRequest> for reqwest::multipart::Form {
impl AsyncTryFrom<CreateImageEditRequest> for reqwest::multipart::Form {
type Error = OpenAIError;

async fn try_from(request: CreateImageEditRequest) -> Result<Self, Self::Error> {
@@ -926,8 +923,7 @@ impl async_convert::TryFrom<CreateImageEditRequest> for reqwest::multipart::Form
}
}

#[async_convert::async_trait]
impl async_convert::TryFrom<CreateImageVariationRequest> for reqwest::multipart::Form {
impl AsyncTryFrom<CreateImageVariationRequest> for reqwest::multipart::Form {
type Error = OpenAIError;

async fn try_from(request: CreateImageVariationRequest) -> Result<Self, Self::Error> {
@@ -961,8 +957,7 @@ impl async_convert::TryFrom<CreateImageVariationRequest> for reqwest::multipart:
}
}

#[async_convert::async_trait]
impl async_convert::TryFrom<CreateFileRequest> for reqwest::multipart::Form {
impl AsyncTryFrom<CreateFileRequest> for reqwest::multipart::Form {
type Error = OpenAIError;

async fn try_from(request: CreateFileRequest) -> Result<Self, Self::Error> {
@@ -974,8 +969,7 @@ impl async_convert::TryFrom<CreateFileRequest> for reqwest::multipart::Form {
}
}

#[async_convert::async_trait]
impl async_convert::TryFrom<AddUploadPartRequest> for reqwest::multipart::Form {
impl AsyncTryFrom<AddUploadPartRequest> for reqwest::multipart::Form {
type Error = OpenAIError;

async fn try_from(request: AddUploadPartRequest) -> Result<Self, Self::Error> {
8 changes: 8 additions & 0 deletions async-openai/src/util.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,14 @@ use tokio_util::codec::{BytesCodec, FramedRead};
use crate::error::OpenAIError;
use crate::types::InputSource;

pub(crate) trait AsyncTryFrom<T>: Sized {
/// The type returned in the event of a conversion error.
type Error;

/// Performs the conversion.
async fn try_from(value: T) -> Result<Self, Self::Error>;
}

pub(crate) async fn file_stream_body(source: InputSource) -> Result<Body, OpenAIError> {
let body = match source {
InputSource::Path { path } => {
12 changes: 12 additions & 0 deletions examples/structured-outputs-schemars/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "structured-outputs-schemars"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
async-openai = {path = "../../async-openai"}
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["full"] }
schemars = "0.8.21"
serde = "1.0.130"
39 changes: 39 additions & 0 deletions examples/structured-outputs-schemars/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## Intro

Based on the 'Chain of thought' example from https://platform.openai.com/docs/guides/structured-outputs/introduction?lang=curl

Using `schemars` and `serde` reduces coding effort.

## Output

```
cargo run | jq .
```

```
{
"final_answer": "x = -3.75",
"steps": [
{
"explanation": "Start with the equation given in the problem.",
"output": "8x + 7 = -23"
},
{
"explanation": "Subtract 7 from both sides to begin isolating the term with the variable x.",
"output": "8x + 7 - 7 = -23 - 7"
},
{
"explanation": "Simplify both sides. On the left-hand side, 7 - 7 equals 0, cancelling out, leaving the equation as follows.",
"output": "8x = -30"
},
{
"explanation": "Now, divide both sides by 8 to fully isolate x.",
"output": "8x/8 = -30/8"
},
{
"explanation": "Simplify the right side by performing the division. -30 divided by 8 is -3.75.",
"output": "x = -3.75"
}
]
}
```
97 changes: 97 additions & 0 deletions examples/structured-outputs-schemars/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::error::Error;

use async_openai::{
types::{
ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage,
ChatCompletionRequestUserMessage, CreateChatCompletionRequestArgs, ResponseFormat,
ResponseFormatJsonSchema,
},
Client,
};
use schemars::{schema_for, JsonSchema};
use serde::{de::DeserializeOwned, Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Step {
pub output: String,
pub explanation: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MathReasoningResponse {
pub final_answer: String,
pub steps: Vec<Step>,
}

pub async fn structured_output<T: serde::Serialize + DeserializeOwned + JsonSchema>(
messages: Vec<ChatCompletionRequestMessage>,
) -> Result<Option<T>, Box<dyn Error>> {
let schema = schema_for!(T);
let schema_value = serde_json::to_value(&schema)?;
let response_format = ResponseFormat::JsonSchema {
json_schema: ResponseFormatJsonSchema {
description: None,
name: "math_reasoning".into(),
schema: Some(schema_value),
strict: Some(true),
},
};

let request = CreateChatCompletionRequestArgs::default()
.max_tokens(512u32)
.model("gpt-4o-mini")
.messages(messages)
.response_format(response_format)
.build()?;

let client = Client::new();
let response = client.chat().create(request).await?;

for choice in response.choices {
if let Some(content) = choice.message.content {
return Ok(Some(serde_json::from_str::<T>(&content)?));
}
}

Ok(None)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Expecting output schema
// let schema = json!({
// "type": "object",
// "properties": {
// "steps": {
// "type": "array",
// "items": {
// "type": "object",
// "properties": {
// "explanation": { "type": "string" },
// "output": { "type": "string" }
// },
// "required": ["explanation", "output"],
// "additionalProperties": false
// }
// },
// "final_answer": { "type": "string" }
// },
// "required": ["steps", "final_answer"],
// "additionalProperties": false
// });
if let Some(response) = structured_output::<MathReasoningResponse>(vec![
ChatCompletionRequestSystemMessage::from(
"You are a helpful math tutor. Guide the user through the solution step by step.",
)
.into(),
ChatCompletionRequestUserMessage::from("how can I solve 8x + 7 = -23").into(),
])
.await?
{
println!("{}", serde_json::to_string(&response).unwrap());
}

Ok(())
}