Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rig-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- *(providers)* [**breaking**] Move Together, OpenRouter, and Mistral embeddings onto the shared `GenericEmbeddingModel`, with provider-specific endpoint and typed request-shaping hooks. Together now forwards configured embedding dimensions, Mistral maps Codestral Embed dimensions to `output_dimension` while rejecting dimensions for fixed-size models, compatible providers may omit usage without weakening OpenAI's public response type, and Base64 response encoding is rejected before sending because the shared parser accepts numeric vectors. Remove the superseded provider-specific embedding response/data types, Together's API envelope module, and OpenRouter's duplicate `EncodingFormat`.
- *(tool)* [**breaking**] Replace the parallel tool-execution APIs with one structured path. Typed tools now implement only `Tool::call(&mut ToolContext, Args) -> Result<Output, Error>`; author-facing errors remain typed until private runtime erasure normalizes them into `ToolExecutionError`, `ToolContext` carries inbound values and host-only result metadata, `ToolResult` is the single runtime observation, and `ToolSet::execute` / `ToolServerHandle::execute` are the dispatch surfaces. Event-specific hook action types make invalid event/action combinations unrepresentable.
- Tool implementations: retain one typed `type Error` for ordinary `?` propagation and direct-call tests; remove `classify_error`, `call_with_extensions`, and `call_structured`. The optional `map_error` method classifies domain failures at the erased boundary, while its default preserves the source as `Other`. Return refusals through `map_error` with `ToolExecutionError::refused`, and attach host-only result metadata with `ToolContext::insert_result`.
- Context: replace `ToolCallExtensions` and `ToolResultExtensions` with `ToolContext`; replace request/runner `.tool_extensions(...)` with `.tool_context(...)`. Each dispatch snapshots inbound context exactly once, isolates tool-local mutations, and publishes only result metadata back to the caller and hooks.
Expand Down
37 changes: 37 additions & 0 deletions crates/rig-core/src/embeddings/embedding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,43 @@ pub enum EmbeddingError {
#[error("ResponseError: {0}")]
ResponseError(String),

/// The provider does not support an embedding request parameter configured on the model.
#[error("{provider} embeddings do not support the `{parameter}` parameter")]
UnsupportedParameter {
/// Provider whose embedding API rejected the parameter.
provider: &'static str,
/// Unsupported request parameter.
parameter: &'static str,
},

/// A provider request parameter was configured with a value outside the
/// provider's supported range.
#[error("{provider} embeddings require `{parameter}` {requirement}")]
InvalidParameterValue {
/// Provider whose embedding API constrains the parameter.
provider: &'static str,
/// Request parameter with the invalid value.
parameter: &'static str,
/// Concise description of the accepted values.
requirement: &'static str,
},

/// Rig cannot decode the requested provider response encoding.
#[error("Rig cannot decode {provider} embedding responses encoded as `{encoding_format}`")]
UnsupportedResponseEncoding {
/// Provider whose response encoding was requested.
provider: &'static str,
/// Response encoding that Rig cannot decode.
encoding_format: &'static str,
},

/// A provider that guarantees embedding usage omitted it from the response.
#[error("{provider} embedding response omitted required usage")]
MissingUsage {
/// Provider whose response omitted usage.
provider: &'static str,
},

/// Error returned by the embedding model provider
#[error("ProviderError: {0}")]
ProviderError(String),
Expand Down
64 changes: 63 additions & 1 deletion crates/rig-core/src/providers/llamafile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ impl openai::completion::OpenAICompatibleProvider for LlamafileExt {
type Response = openai::CompletionResponse;
}

impl openai::embedding::OpenAIEmbeddingsCompatible for LlamafileExt {
const PROVIDER_NAME: &'static str = "llamafile";
}

impl<H> Capabilities<H> for LlamafileExt {
type Completion = Capable<openai::completion::GenericCompletionModel<LlamafileExt, H>>;
type Embeddings = Capable<openai::embedding::GenericEmbeddingModel<LlamafileExt, H>>;
Expand Down Expand Up @@ -145,7 +149,10 @@ impl ProviderClient for Client {
#[cfg(test)]
mod tests {
use super::*;
use crate::client::Nothing;
use crate::client::{EmbeddingsClient, Nothing};
use crate::embeddings::EmbeddingModel as _;
use crate::providers::openai::embedding::EncodingFormat;
use crate::test_utils::RecordingHttpClient;

#[test]
fn test_client_initialization() {
Expand Down Expand Up @@ -186,4 +193,59 @@ mod tests {
"http://localhost:8080/v1/models"
);
}

#[tokio::test]
async fn embedding_model_preserves_v1_path_and_usage() {
let response = r#"{
"object": "list",
"model": "LLaMA_CPP",
"usage": { "prompt_tokens": 2, "total_tokens": 2 },
"data": [{ "object": "embedding", "index": 0, "embedding": [0.1, 0.2] }]
}"#;
let http_client = RecordingHttpClient::new(response);
let client = Client::builder()
.api_key(Nothing)
.http_client(http_client.clone())
.build()
.expect("client should build");
let model = client.embedding_model(LLAMA_CPP);

let response = model
.embed_texts_with_usage(["hello".to_string()])
.await
.expect("embedding request should succeed");

assert_eq!(response.usage.total_tokens, 2);
assert_eq!(
http_client.requests()[0].uri,
"http://localhost:8080/v1/embeddings"
);
}

#[tokio::test]
async fn embedding_model_rejects_base64_before_sending() {
let http_client = RecordingHttpClient::new("{}");
let client = Client::builder()
.api_key(Nothing)
.http_client(http_client.clone())
.build()
.expect("client should build");
let model = client
.embedding_model(LLAMA_CPP)
.encoding_format(EncodingFormat::Base64);

let error = model
.embed_texts(["hello".to_string()])
.await
.expect_err("numeric response parser should reject base64");

assert!(matches!(
error,
crate::embeddings::EmbeddingError::UnsupportedResponseEncoding {
provider: "llamafile",
encoding_format: "base64"
}
));
assert!(http_client.requests().is_empty());
}
}
12 changes: 0 additions & 12 deletions crates/rig-core/src/providers/mistral/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,18 +217,6 @@ impl std::fmt::Display for Usage {
}
}

#[derive(Debug, Deserialize)]
pub struct ApiErrorResponse {
pub(crate) message: String,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum ApiResponse<T> {
Ok(T),
Err(ApiErrorResponse),
}

#[cfg(test)]
mod tests {
#[test]
Expand Down
Loading
Loading