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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`resolveProviderReference`, which takes the reference and the provider
name the same way (RFC-0046 R1).

- `aimux_providers::anthropic::tool_name_mapping` is removed. Its
`ToolNameMapping` is `aimux_provider_utils::ToolNameMapping`, built with
`aimux_provider_utils::create_tool_name_mapping(tools, provider_tool_names)`
and the provider tool id table, as the AI SDK's `createToolNameMapping`;
`to_provider_tool_name` and `to_custom_tool_name` are unchanged.
`mark_code_execution_dynamic` has no public replacement (RFC-0046 R1).

- `aimux_providers::mistral` follows the upstream `@ai-sdk/mistral` file
layout (RFC-0046). The sub-modules `mistral::convert` and
`mistral::embedding` are no longer public; use `MistralEmbeddingModel`,
Expand Down Expand Up @@ -1425,9 +1432,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
xAI, Anthropic, Google and DeepSeek converters resolve file references with
the second. A missing DeepSeek reference now names the providers the
reference has, as the AI SDK's `NoSuchProviderReferenceError` does.
- `aimux_provider_utils::create_tool_name_mapping` with `ToolNameMapping`,
and `aimux_provider_utils::strip_file_extension`, as
`@ai-sdk/provider-utils` 5.0.55 (RFC-0046 R1). The Anthropic, OpenAI
Responses and Google models map provider tool names with the first, as
their upstream models do.

### Fixed

- Bedrock document names drop everything from the first dot of the filename,
as the AI SDK's `stripFileExtension` does: `report.v2.pdf` is sent as
`report`, not `reportv2`.
- Anthropic AWS models are named `anthropic-aws.messages` by default, the
provider name the AI SDK gives them, instead of `anthropic-aws`. A `name`
setting still replaces it, and the providerOptions key stays `anthropic`.
Expand Down
67 changes: 67 additions & 0 deletions aimux-provider-utils/src/create_tool_name_mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! `createToolNameMapping` from `@ai-sdk/provider-utils`
//! (`create-tool-name-mapping.ts`).
//!
//! A [`ProviderTool`](aimux_core::tool::ProviderTool) carries both an `id`
//! (`anthropic.web_search_20250305`) and a `name`, the name the caller uses
//! for it in this call. The provider's wire format uses its own fixed name
//! (`web_search`), so the two differ whenever the caller renames a provider
//! tool: prompts map the caller's names to the provider's, responses map them
//! back.

use std::collections::HashMap;

use aimux_core::tool::Tool;

/// Maps between custom tool names and provider tool names.
#[derive(Debug, Default, Clone)]
pub struct ToolNameMapping {
custom_tool_name_to_provider_tool_name: HashMap<String, String>,
provider_tool_name_to_custom_tool_name: HashMap<String, String>,
}

impl ToolNameMapping {
/// Maps a custom tool name (used by the client) to the provider's tool
/// name. A name without a mapping is returned as given.
#[must_use]
pub fn to_provider_tool_name<'a>(&'a self, custom_tool_name: &'a str) -> &'a str {
self.custom_tool_name_to_provider_tool_name
.get(custom_tool_name)
.map_or(custom_tool_name, String::as_str)
}

/// Maps a provider tool name to the custom tool name used by the client.
/// A name without a mapping is returned as given.
#[must_use]
pub fn to_custom_tool_name<'a>(&'a self, provider_tool_name: &'a str) -> &'a str {
self.provider_tool_name_to_custom_tool_name
.get(provider_tool_name)
.map_or(provider_tool_name, String::as_str)
}
}

/// Builds the mapping for the provider tools of one call.
///
/// `tools` are the tools passed to the language model; `provider_tool_names`
/// maps provider tool ids (`provider.tool`) to the provider tool names.
/// Function tools and provider tools whose id is not listed are not mapped.
#[must_use]
pub fn create_tool_name_mapping(
tools: Option<&[Tool]>,
provider_tool_names: &[(&str, &str)],
) -> ToolNameMapping {
let mut mapping = ToolNameMapping::default();
for tool in tools.unwrap_or_default() {
if let Tool::Provider(tool) = tool
&& let Some((_, provider_tool_name)) =
provider_tool_names.iter().find(|(id, _)| tool.id == *id)
{
mapping
.custom_tool_name_to_provider_tool_name
.insert(tool.name.clone(), (*provider_tool_name).to_owned());
mapping
.provider_tool_name_to_custom_tool_name
.insert((*provider_tool_name).to_owned(), tool.name.clone());
}
}
mapping
}
4 changes: 4 additions & 0 deletions aimux-provider-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod api_key;
pub mod convert_image_model_file_to_data_uri;
pub mod convert_inline_file_data_to_uint8_array;
pub mod create_language_model_response_metadata;
pub mod create_tool_name_mapping;
pub mod decision_language_model;
pub mod download_blob;
pub mod extract_response_headers;
Expand All @@ -33,6 +34,7 @@ pub mod resolvable;
pub mod resolve_provider_reference;
pub mod response_handler;
pub mod sigv4_fetch;
pub mod strip_file_extension;
#[cfg(test)]
mod tests;
pub mod transform_stream;
Expand All @@ -48,6 +50,7 @@ pub use api_key::{load_api_key, load_optional_setting, load_setting};
pub use convert_image_model_file_to_data_uri::convert_image_model_file_to_data_uri;
pub use convert_inline_file_data_to_uint8_array::convert_inline_file_data_to_uint8_array;
pub use create_language_model_response_metadata::create_language_model_response_metadata;
pub use create_tool_name_mapping::{ToolNameMapping, create_tool_name_mapping};
pub use decision_language_model::DecisionLanguageModel;
pub use download_blob::download_blob;
pub use fetch::{
Expand Down Expand Up @@ -77,6 +80,7 @@ pub use response_handler::{
create_status_code_error_response_handler, invalid_response_api_call, stream_error_api_call,
};
pub use sigv4_fetch::{AwsCredentials, SigV4Fetch, SigV4Signing};
pub use strip_file_extension::strip_file_extension;
pub use transform_stream::{PipeThrough, TransformStreamController, Transformer, pipe_through};
pub use uint8_utils::{
Base64OrBytes, InvalidBase64Error, convert_base64_to_uint8_array, convert_to_base64,
Expand Down
13 changes: 13 additions & 0 deletions aimux-provider-utils/src/strip_file_extension.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! `stripFileExtension` from `@ai-sdk/provider-utils`
//! (`strip-file-extension.ts`).

/// Strips every extension segment from a filename: everything from the first
/// dot on.
///
/// - `"report.pdf"` -> `"report"`
/// - `"archive.tar.gz"` -> `"archive"`
/// - `"filename"` -> `"filename"`
#[must_use]
pub fn strip_file_extension(filename: &str) -> &str {
filename.split_once('.').map_or(filename, |(name, _)| name)
}
142 changes: 142 additions & 0 deletions aimux-provider-utils/src/tests/create_tool_name_mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! Rust translation of `packages/provider-utils/src/create-tool-name-mapping.test.ts`
//! (@ai-sdk/provider-utils).

use aimux_core::tool::{FunctionTool, ProviderTool, Tool};
use aimux_core::types::NamespacedKind;
use serde_json::{Map, json};

use crate::create_tool_name_mapping;

fn provider_tool(id: &str, name: &str) -> Tool {
Tool::Provider(ProviderTool {
id: NamespacedKind::new(id).unwrap(),
name: name.to_owned(),
args: Map::new(),
supports_deferred_results: None,
})
}

fn function_tool(name: &str) -> Tool {
Tool::Function(
FunctionTool::new(name, json!({ "type": "object" })).with_description("A function tool"),
)
}

/// upstream: @ai-sdk/provider-utils src/create-tool-name-mapping.test.ts › createToolNameMapping › should create mappings for provider-defined tools
#[test]
fn should_create_mappings_for_provider_defined_tools() {
let tools = [
provider_tool("anthropic.computer-use", "custom-computer-tool"),
provider_tool("openai.code-interpreter", "custom-code-tool"),
];
let provider_tool_names = [
("anthropic.computer-use", "computer_use"),
("openai.code-interpreter", "code_interpreter"),
];

let mapping = create_tool_name_mapping(Some(&tools), &provider_tool_names);

assert_eq!(
mapping.to_provider_tool_name("custom-computer-tool"),
"computer_use"
);
assert_eq!(
mapping.to_provider_tool_name("custom-code-tool"),
"code_interpreter"
);
assert_eq!(
mapping.to_custom_tool_name("computer_use"),
"custom-computer-tool"
);
assert_eq!(
mapping.to_custom_tool_name("code_interpreter"),
"custom-code-tool"
);
}

/// upstream: @ai-sdk/provider-utils src/create-tool-name-mapping.test.ts › createToolNameMapping › should ignore function tools
#[test]
fn should_ignore_function_tools() {
let tools = [function_tool("my-function-tool")];

let mapping = create_tool_name_mapping(Some(&tools), &[]);

assert_eq!(
mapping.to_provider_tool_name("my-function-tool"),
"my-function-tool"
);
assert_eq!(
mapping.to_custom_tool_name("my-function-tool"),
"my-function-tool"
);
}

/// upstream: @ai-sdk/provider-utils src/create-tool-name-mapping.test.ts › createToolNameMapping › should return input name when tool is not in providerToolNames
#[test]
fn should_return_input_name_when_tool_is_not_in_provider_tool_names() {
let tools = [provider_tool("unknown.tool", "custom-tool")];

let mapping = create_tool_name_mapping(Some(&tools), &[]);

assert_eq!(mapping.to_provider_tool_name("custom-tool"), "custom-tool");
assert_eq!(mapping.to_custom_tool_name("unknown-name"), "unknown-name");
}

/// upstream: @ai-sdk/provider-utils src/create-tool-name-mapping.test.ts › createToolNameMapping › should return input name when mapping does not exist
#[test]
fn should_return_input_name_when_mapping_does_not_exist() {
let tools = [provider_tool(
"anthropic.computer-use",
"custom-computer-tool",
)];
let provider_tool_names = [("anthropic.computer-use", "computer_use")];

let mapping = create_tool_name_mapping(Some(&tools), &provider_tool_names);

assert_eq!(
mapping.to_provider_tool_name("non-existent-tool"),
"non-existent-tool"
);
assert_eq!(
mapping.to_custom_tool_name("non-existent-provider-tool"),
"non-existent-provider-tool"
);
}

/// upstream: @ai-sdk/provider-utils src/create-tool-name-mapping.test.ts › createToolNameMapping › should handle empty tools array
#[test]
fn should_handle_empty_tools_array() {
let mapping = create_tool_name_mapping(Some(&[]), &[]);

assert_eq!(mapping.to_provider_tool_name("any-tool"), "any-tool");
assert_eq!(mapping.to_custom_tool_name("any-tool"), "any-tool");
}

/// upstream: @ai-sdk/provider-utils src/create-tool-name-mapping.test.ts › createToolNameMapping › should handle mixed function and provider-defined tools
#[test]
fn should_handle_mixed_function_and_provider_defined_tools() {
let tools = [
function_tool("function-tool"),
provider_tool("anthropic.computer-use", "provider-tool"),
];
let provider_tool_names = [("anthropic.computer-use", "computer_use")];

let mapping = create_tool_name_mapping(Some(&tools), &provider_tool_names);

// Function tool should not be mapped
assert_eq!(
mapping.to_provider_tool_name("function-tool"),
"function-tool"
);
assert_eq!(
mapping.to_custom_tool_name("function-tool"),
"function-tool"
);

// Provider-defined tool should be mapped
assert_eq!(
mapping.to_provider_tool_name("provider-tool"),
"computer_use"
);
assert_eq!(mapping.to_custom_tool_name("computer_use"), "provider-tool");
}
2 changes: 2 additions & 0 deletions aimux-provider-utils/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@

mod convert_image_model_file_to_data_uri;
mod convert_inline_file_data_to_uint8_array;
mod create_tool_name_mapping;
mod resolve_provider_reference;
mod strip_file_extension;
mod uint8_utils;
28 changes: 28 additions & 0 deletions aimux-provider-utils/src/tests/strip_file_extension.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Rust translation of `packages/provider-utils/src/strip-file-extension.test.ts`
//! (@ai-sdk/provider-utils).

use crate::strip_file_extension;

/// upstream: @ai-sdk/provider-utils src/strip-file-extension.test.ts › stripFileExtension › should strip the extension from a filename
#[test]
fn should_strip_the_extension_from_a_filename() {
assert_eq!(strip_file_extension("report.pdf"), "report");
}

/// upstream: @ai-sdk/provider-utils src/strip-file-extension.test.ts › stripFileExtension › should return the input when there is no extension
#[test]
fn should_return_the_input_when_there_is_no_extension() {
assert_eq!(strip_file_extension("report"), "report");
}

/// upstream: @ai-sdk/provider-utils src/strip-file-extension.test.ts › stripFileExtension › should strip all extension segments for multi-dot filenames
#[test]
fn should_strip_all_extension_segments_for_multi_dot_filenames() {
assert_eq!(strip_file_extension("archive.tar.gz"), "archive");
}

/// upstream: @ai-sdk/provider-utils src/strip-file-extension.test.ts › stripFileExtension › should strip a trailing dot
#[test]
fn should_strip_a_trailing_dot() {
assert_eq!(strip_file_extension("report."), "report");
}
Loading
Loading