10 Commits

Author SHA1 Message Date
Ivan Gabriele
9430d42382 ci(release): v0.7.0 2024-03-05 02:49:59 +01:00
Ivan Gabriele
e7d844dce9 docs(changelog): update 2024-03-05 02:49:52 +01:00
Ivan Gabriele
29566f7948 ci(github): split documentation tests into a separate job 2024-03-05 02:48:24 +01:00
Ivan Gabriele
72bae8817a docs: add client.chat*() documentation 2024-03-05 02:40:49 +01:00
Ivan Gabriele
08b042506d test(coverage): migrate from tarpaulin to llvm-cov 2024-03-05 02:34:50 +01:00
Ivan Gabriele
efcd93953a build(makefile): add --skip-clean option to test-cover command 2024-03-05 01:36:02 +01:00
Ivan Gabriele
ea99a075ef build(makefile): remove wrong --nocapture option from test-doc command 2024-03-05 00:59:13 +01:00
Ivan Gabriele
ccf3d1431a build(makefile): add doc command 2024-03-05 00:55:07 +01:00
Ivan Gabriele
a8bfb5333f ci(github): add documentation tests 2024-03-05 00:50:21 +01:00
Ivan Gabriele
ef5d475e2d fix!: fix failure when api key as param and not env
BREAKING CHANGE:

- Rename `ClientError.ApiKeyError` to `MissingApiKey`.
- Rename `ClientError.ReadResponseTextError` to `ClientError.UnreadableResponseText`.
2024-03-04 21:12:08 +01:00
10 changed files with 227 additions and 19 deletions

View File

@@ -6,10 +6,6 @@ jobs:
test:
name: Test
runs-on: ubuntu-latest
container:
image: xd009642/tarpaulin
# https://github.com/xd009642/tarpaulin#github-actions
options: --security-opt seccomp=unconfined
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -17,12 +13,30 @@ jobs:
uses: actions-rs/toolchain@v1
with:
toolchain: 1.76.0
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run tests (with coverage)
run: make test-cover
run: cargo llvm-cov --lcov --output-path ./lcov.info
env:
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
- name: Upload tests coverage
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
files: ./lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
test_documentation:
name: Test Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.76.0
- name: Run documentation tests
run: make test-doc
env:
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}

View File

@@ -1,3 +1,15 @@
## [0.7.0](https://github.com/ivangabriele/mistralai-client-rs/compare/v0.6.0...v) (2024-03-05)
### ⚠ BREAKING CHANGES
* - Rename `ClientError.ApiKeyError` to `MissingApiKey`.
- Rename `ClientError.ReadResponseTextError` to `ClientError.UnreadableResponseText`.
### Bug Fixes
* fix failure when api key as param and not env ([ef5d475](https://github.com/ivangabriele/mistralai-client-rs/commit/ef5d475e2d0e3fe040c44d6adabf7249e9962835))
## [0.6.0](https://github.com/ivangabriele/mistralai-client-rs/compare/v0.5.0...v) (2024-03-04)

View File

@@ -38,7 +38,8 @@ Then edit the `.env` file to set your `MISTRAL_API_KEY`.
### Optional requirements
- [cargo-watch](https://github.com/watchexec/cargo-watch#install) for `make test-*-watch`.
- [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov?tab=readme-ov-file#installation) for `make test-cover`
- [cargo-watch](https://github.com/watchexec/cargo-watch#install) for `make test-watch`.
### Test

View File

@@ -2,7 +2,7 @@
name = "mistralai-client"
description = "Mistral AI API client library for Rust (unofficial)."
license = "Apache-2.0"
version = "0.6.0"
version = "0.7.0"
edition = "2021"
rust-version = "1.76.0"
@@ -19,6 +19,8 @@ futures = "0.3.30"
reqwest = { version = "0.11.24", features = ["json", "blocking", "stream"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
strum = "0.26.1"
strum_macros = "0.26.1"
thiserror = "1.0.57"
tokio = { version = "1.36.0", features = ["full"] }

View File

@@ -2,6 +2,17 @@ SHELL := /bin/bash
.PHONY: test
define source_env_if_not_ci
@if [ -z "$${CI}" ]; then \
if [ -f ./.env ]; then \
source ./.env; \
else \
echo "No .env file found"; \
exit 1; \
fi \
fi
endef
define RELEASE_TEMPLATE
conventional-changelog -p conventionalcommits -i ./CHANGELOG.md -s
git add .
@@ -11,6 +22,10 @@ define RELEASE_TEMPLATE
git push origin HEAD --tags
endef
doc:
cargo doc
open ./target/doc/mistralai_client/index.html
release-patch:
$(call RELEASE_TEMPLATE,patch)
@@ -21,8 +36,10 @@ release-major:
$(call RELEASE_TEMPLATE,major)
test:
@source ./.env && cargo test --all-targets --no-fail-fast
@$(source_env_if_not_ci) && cargo test --no-fail-fast
test-cover:
cargo tarpaulin --all-targets --frozen --no-fail-fast --out Xml --skip-clean
@$(source_env_if_not_ci) && cargo llvm-cov
test-doc:
@$(source_env_if_not_ci) && cargo test --doc --no-fail-fast
test-watch:
cargo watch -x "test -- --all-targets --nocapture"
@source ./.env && cargo watch -x "test -- --nocapture"

View File

@@ -1 +1,4 @@
//! This crate provides a easy bindings and types for MistralAI's API.
/// The v1 module contains the types and methods for the v1 API endpoints.
pub mod v1;

View File

@@ -11,7 +11,7 @@ pub struct ChatMessage {
pub content: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Debug, strum_macros::Display, Eq, PartialEq, Deserialize, Serialize)]
#[allow(non_camel_case_types)]
pub enum ChatMessageRole {
assistant,

View File

@@ -25,16 +25,39 @@ pub struct Client {
}
impl Client {
/// Constructs a new `Client`.
///
/// # Arguments
///
/// * `api_key` - An optional API key.
/// If not provided, the method will try to use the `MISTRAL_API_KEY` environment variable.
/// * `endpoint` - An optional custom API endpoint. Defaults to the official API endpoint if not provided.
/// * `max_retries` - Optional maximum number of retries for failed requests. Defaults to `5`.
/// * `timeout` - Optional timeout in seconds for requests. Defaults to `120`.
///
/// # Examples
///
/// ```
/// use mistralai_client::v1::client::Client;
///
/// let client = Client::new(Some("your_api_key_here".to_string()), None, Some(3), Some(60));
/// assert!(client.is_ok());
/// ```
///
/// # Errors
///
/// This method fails whenever neither the `api_key` is provided
/// nor the `MISTRAL_API_KEY` environment variable is set.
pub fn new(
api_key: Option<String>,
endpoint: Option<String>,
max_retries: Option<u32>,
timeout: Option<u32>,
) -> Result<Self, ClientError> {
let api_key = api_key.unwrap_or(match std::env::var("MISTRAL_API_KEY") {
Ok(api_key_from_env) => api_key_from_env,
Err(_) => return Err(ClientError::ApiKeyError),
});
let api_key = match api_key {
Some(api_key_from_param) => api_key_from_param,
None => std::env::var("MISTRAL_API_KEY").map_err(|_| ClientError::MissingApiKey)?,
};
let endpoint = endpoint.unwrap_or(API_URL_BASE.to_string());
let max_retries = max_retries.unwrap_or(5);
let timeout = timeout.unwrap_or(120);
@@ -47,6 +70,36 @@ impl Client {
})
}
/// Synchronously sends a chat completion request and returns the response.
///
/// # Arguments
///
/// * `model` - The [Model] to use for the chat completion.
/// * `messages` - A vector of [ChatMessage] to send as part of the chat.
/// * `options` - Optional [ChatCompletionParams] to customize the request.
///
/// # Returns
///
/// Returns a [Result] containing the `ChatCompletionResponse` if the request is successful,
/// or an [ApiError] if there is an error.
///
/// # Examples
///
/// ```
/// use mistralai_client::v1::{
/// chat_completion::{ChatMessage, ChatMessageRole},
/// client::Client,
/// constants::Model,
/// };
///
/// let client = Client::new(None, None, None, None).unwrap();
/// let messages = vec![ChatMessage {
/// role: ChatMessageRole::user,
/// content: "Hello, world!".to_string(),
/// }];
/// let response = client.chat(Model::OpenMistral7b, messages, None).unwrap();
/// println!("{}: {}", response.choices[0].message.role, response.choices[0].message.content);
/// ```
pub fn chat(
&self,
model: Model,
@@ -63,6 +116,39 @@ impl Client {
}
}
/// Asynchronously sends a chat completion request and returns the response.
///
/// # Arguments
///
/// * `model` - The [Model] to use for the chat completion.
/// * `messages` - A vector of [ChatMessage] to send as part of the chat.
/// * `options` - Optional [ChatCompletionParams] to customize the request.
///
/// # Returns
///
/// Returns a [Result] containing a `Stream` of `ChatCompletionStreamChunk` if the request is successful,
/// or an [ApiError] if there is an error.
///
/// # Examples
///
/// ```
/// use mistralai_client::v1::{
/// chat_completion::{ChatMessage, ChatMessageRole},
/// client::Client,
/// constants::Model,
/// };
///
/// #[tokio::main]
/// async fn main() {
/// let client = Client::new(None, None, None, None).unwrap();
/// let messages = vec![ChatMessage {
/// role: ChatMessageRole::user,
/// content: "Hello, world!".to_string(),
/// }];
/// let response = client.chat_async(Model::OpenMistral7b, messages, None).await.unwrap();
/// println!("{}: {}", response.choices[0].message.role, response.choices[0].message.content);
/// }
/// ```
pub async fn chat_async(
&self,
model: Model,
@@ -79,6 +165,48 @@ impl Client {
}
}
/// Asynchronously sends a chat completion request and returns a stream of message chunks.
///
/// # Arguments
///
/// * `model` - The [Model] to use for the chat completion.
/// * `messages` - A vector of [ChatMessage] to send as part of the chat.
/// * `options` - Optional [ChatCompletionParams] to customize the request.
///
/// # Returns
///
/// Returns a [Result] containing a `Stream` of `ChatCompletionStreamChunk` if the request is successful,
/// or an [ApiError] if there is an error.
///
/// # Examples
///
/// ```
/// use futures::stream::StreamExt;
/// use mistralai_client::v1::{
/// chat_completion::{ChatMessage, ChatMessageRole},
/// client::Client,
/// constants::Model,
/// };
///
/// #[tokio::main]
/// async fn main() {
/// let client = Client::new(None, None, None, None).unwrap();
/// let messages = vec![ChatMessage {
/// role: ChatMessageRole::user,
/// content: "Hello, world!".to_string(),
/// }];
/// let mut stream = client.chat_stream(Model::OpenMistral7b, messages, None).await.unwrap();
/// while let Some(chunk_result) = stream.next().await {
/// match chunk_result {
/// Ok(chunk) => {
/// print!("{}", chunk.choices[0].delta.content);
/// }
/// Err(error) => {
/// println!("Error: {}", error.message);
/// }
/// }
/// }
/// }
pub async fn chat_stream(
&self,
model: Model,

View File

@@ -15,7 +15,7 @@ impl Error for ApiError {}
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum ClientError {
#[error("You must either set the `MISTRAL_API_KEY` environment variable or specify it in `Client::new(api_key, ...).")]
ApiKeyError,
MissingApiKey,
#[error("Failed to read the response text.")]
ReadResponseTextError,
UnreadableResponseText,
}

View File

@@ -26,6 +26,37 @@ fn test_client_new_with_none_params() {
fn test_client_new_with_all_params() {
let maybe_original_mistral_api_key = std::env::var("MISTRAL_API_KEY").ok();
std::env::remove_var("MISTRAL_API_KEY");
let api_key = Some("test_api_key_from_param".to_string());
let endpoint = Some("https://example.org".to_string());
let max_retries = Some(10);
let timeout = Some(20);
let client = Client::new(
api_key.clone(),
endpoint.clone(),
max_retries.clone(),
timeout.clone(),
)
.unwrap();
expect!(client.api_key).to_be(api_key.unwrap());
expect!(client.endpoint).to_be(endpoint.unwrap());
expect!(client.max_retries).to_be(max_retries.unwrap());
expect!(client.timeout).to_be(timeout.unwrap());
match maybe_original_mistral_api_key {
Some(original_mistral_api_key) => {
std::env::set_var("MISTRAL_API_KEY", original_mistral_api_key)
}
None => std::env::remove_var("MISTRAL_API_KEY"),
}
}
#[test]
fn test_client_new_with_api_key_as_both_env_and_param() {
let maybe_original_mistral_api_key = std::env::var("MISTRAL_API_KEY").ok();
std::env::remove_var("MISTRAL_API_KEY");
std::env::set_var("MISTRAL_API_KEY", "test_api_key_from_env");
let api_key = Some("test_api_key_from_param".to_string());
@@ -62,8 +93,8 @@ fn test_client_new_with_missing_api_key() {
let call = || Client::new(None, None, None, None);
match call() {
Ok(_) => panic!("Expected `ClientError::ApiKeyError` but got Ok.`"),
Err(error) => assert_eq!(error, ClientError::ApiKeyError),
Ok(_) => panic!("Expected `ClientError::MissingApiKey` but got Ok.`"),
Err(error) => assert_eq!(error, ClientError::MissingApiKey),
}
match maybe_original_mistral_api_key {