blob: 5ace5967f085b4fb9520913c5a1a1bd6796eb95c [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{CacheBudget, CacheDirective, CacheSite, Warning};
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize)]
pub struct FunctionTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheDirective>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub eager_input_streaming: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_callers: Option<Vec<CallerKind>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_examples: Option<Vec<Value>>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub enum CallerKind {
#[serde(rename = "direct")]
Direct,
#[serde(rename = "code_execution_20250825")]
CodeExecution20250825,
#[serde(rename = "code_execution_20260120")]
CodeExecution20260120,
}
#[derive(Debug, Clone)]
pub enum Tool {
Function(FunctionTool),
Hosted(HostedTool),
Unsupported(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Feature {
StructuredOutputs,
AdvancedTools,
CodeExecution20250522,
CodeExecution20250825,
Computer20241022,
Computer20250124,
Computer20251124,
ContextManagement,
WebFetch,
CodeExecutionWebTools,
Advisor,
}
impl Feature {
pub fn header(self) -> &'static str {
match self {
Self::StructuredOutputs => "structured-outputs-2025-11-13",
Self::AdvancedTools => "advanced-tool-use-2025-11-20",
Self::CodeExecution20250522 => "code-execution-2025-05-22",
Self::CodeExecution20250825 => "code-execution-2025-08-25",
Self::Computer20241022 => "computer-use-2024-10-22",
Self::Computer20250124 => "computer-use-2025-01-24",
Self::Computer20251124 => "computer-use-2025-11-24",
Self::ContextManagement => "context-management-2025-06-27",
Self::WebFetch => "web-fetch-2025-09-10",
Self::CodeExecutionWebTools => "code-execution-web-tools-2026-02-09",
Self::Advisor => "advisor-tool-2026-03-01",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ToolPolicy {
pub structured_output: bool,
pub strict: bool,
pub eager_streaming: bool,
pub forced_choice: bool,
}
impl Default for ToolPolicy {
fn default() -> Self {
Self {
structured_output: true,
strict: true,
eager_streaming: false,
forced_choice: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolChoice {
Auto,
None,
Required,
Named(String),
}
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WireChoice {
Auto {
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
Any {
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
Tool {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
None,
}
/// Prepared once, then serialized directly into the Messages request body.
#[derive(Debug)]
pub struct PreparedTools {
pub tools: Vec<PreparedTool>,
pub choice: Option<WireChoice>,
pub warnings: Vec<Warning>,
pub features: Vec<Feature>,
}
#[derive(Debug)]
pub enum PreparedTool {
Function(FunctionTool),
Hosted(HostedTool),
}
impl PreparedTools {
pub fn build(
tools: impl IntoIterator<Item = Tool>,
choice: Option<ToolChoice>,
disable_parallel: Option<bool>,
policy: ToolPolicy,
cache: &mut CacheBudget,
) -> Self {
let mut result = Self {
tools: Vec::new(),
choice: None,
warnings: Vec::new(),
features: Vec::new(),
};
let tools: Vec<_> = tools.into_iter().collect();
if tools.is_empty() {
return result;
}
for tool in tools {
match tool {
Tool::Function(mut tool) => {
tool.cache_control =
cache.reserve(tool.cache_control, CacheSite::ToolDefinition);
tool.eager_input_streaming = tool
.eager_input_streaming
.unwrap_or(policy.eager_streaming)
.then_some(true);
if !policy.strict {
if tool.strict == Some(true) {
result.warnings.push(Warning::StrictToolIgnored {
name: tool.name.clone(),
});
}
tool.strict = None;
}
if policy.structured_output {
result.feature(Feature::StructuredOutputs);
}
if tool.allowed_callers.is_some() || tool.input_examples.is_some() {
result.feature(Feature::AdvancedTools);
}
result.tools.push(PreparedTool::Function(tool));
}
Tool::Hosted(tool) => {
if let Some(feature) = tool.feature() {
result.feature(feature);
}
result.tools.push(PreparedTool::Hosted(tool));
}
Tool::Unsupported(id) => result.warnings.push(Warning::UnsupportedTool { id }),
}
}
result.choice = match choice {
None => disable_parallel.filter(|v| *v).map(|v| WireChoice::Auto {
disable_parallel_tool_use: Some(v),
}),
Some(ToolChoice::None) => Some(WireChoice::None),
Some(ToolChoice::Auto) => Some(WireChoice::Auto {
disable_parallel_tool_use: disable_parallel,
}),
Some(ToolChoice::Required) if policy.forced_choice => Some(WireChoice::Any {
disable_parallel_tool_use: disable_parallel,
}),
Some(ToolChoice::Named(name)) if policy.forced_choice => Some(WireChoice::Tool {
name,
disable_parallel_tool_use: disable_parallel,
}),
Some(choice) => {
let name = match choice {
ToolChoice::Named(name) => Some(name),
_ => None,
};
if let Some(name) = &name {
result
.tools
.retain(|tool| tool.name() == Some(name.as_str()));
}
result
.warnings
.push(Warning::ForcedToolChoiceIgnored { name });
Some(WireChoice::Auto {
disable_parallel_tool_use: disable_parallel,
})
}
};
result
}
fn feature(&mut self, feature: Feature) {
if !self.features.contains(&feature) {
self.features.push(feature);
}
}
pub fn wire_tools(&self) -> Vec<Value> {
self.tools.iter().map(PreparedTool::wire).collect()
}
}
impl PreparedTool {
fn name(&self) -> Option<&str> {
match self {
Self::Function(tool) => Some(&tool.name),
Self::Hosted(tool) => tool.name(),
}
}
fn wire(&self) -> Value {
match self {
Self::Function(tool) => {
serde_json::to_value(tool).expect("function tool is JSON serializable")
}
Self::Hosted(tool) => tool.wire(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum CodeVersion {
V20250522,
V20250825,
V20260120,
}
#[derive(Debug, Clone, Copy)]
pub enum ComputerVersion {
V20241022,
V20250124,
V20251124,
}
#[derive(Debug, Clone, Copy)]
pub enum EditorVersion {
V20241022,
V20250124,
V20250429,
V20250728,
}
#[derive(Debug, Clone, Copy)]
pub enum BashVersion {
V20241022,
V20250124,
}
#[derive(Debug, Clone, Copy)]
pub enum FetchVersion {
V20250910,
V20260209,
V20260318,
}
#[derive(Debug, Clone, Copy)]
pub enum SearchVersion {
V20250305,
V20260209,
V20260318,
}
#[derive(Debug, Clone, Copy)]
pub enum SearchAlgorithm {
Regex,
Bm25,
}
#[derive(Debug, Clone)]
pub enum HostedTool {
CodeExecution(CodeVersion),
Computer(ComputerVersion, ComputerOptions),
Editor(EditorVersion, EditorOptions),
Bash(BashVersion),
ComputerToolset(BTreeMap<ComputerAction, MemberOptions>),
Memory,
WebFetch(FetchVersion, FetchOptions),
WebSearch(SearchVersion, SearchOptions),
ToolSearch(SearchAlgorithm),
Advisor(AdvisorOptions),
}
// Deserialize provider options once at the external configuration boundary.
// The request builder only receives valid, typed definitions.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all(deserialize = "camelCase", serialize = "snake_case"))]
pub struct ComputerOptions {
pub display_width_px: u64,
pub display_height_px: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_number: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_zoom: Option<bool>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all(deserialize = "camelCase", serialize = "snake_case"))]
pub struct EditorOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_characters: Option<serde_json::Number>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ComputerAction {
Screenshot,
Zoom,
LeftClick,
RightClick,
MiddleClick,
DoubleClick,
TripleClick,
LeftClickDrag,
MouseMove,
LeftMouseDown,
LeftMouseUp,
CursorPosition,
Scroll,
Type,
Key,
HoldKey,
Wait,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all(deserialize = "camelCase", serialize = "snake_case"))]
pub struct MemberOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all(deserialize = "camelCase", serialize = "snake_case"))]
pub struct WebLimits {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_uses: Option<serde_json::Number>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_domains: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blocked_domains: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Citations {
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseInclusion {
Full,
Excluded,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all(deserialize = "camelCase", serialize = "snake_case"))]
pub struct FetchOptions {
#[serde(flatten)]
pub limits: WebLimits,
#[serde(skip_serializing_if = "Option::is_none")]
pub citations: Option<Citations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_content_tokens: Option<serde_json::Number>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_cache: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_inclusion: Option<ResponseInclusion>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UserLocation {
Approximate {
#[serde(skip_serializing_if = "Option::is_none")]
city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
region: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
timezone: Option<String>,
},
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all(deserialize = "camelCase", serialize = "snake_case"))]
pub struct SearchOptions {
#[serde(flatten)]
pub limits: WebLimits,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_location: Option<UserLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_inclusion: Option<ResponseInclusion>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(try_from = "u64", into = "u64")]
pub struct AdvisorBudget(u64);
impl TryFrom<u64> for AdvisorBudget {
type Error = &'static str;
fn try_from(value: u64) -> std::result::Result<Self, Self::Error> {
if value >= 1024 {
Ok(Self(value))
} else {
Err("advisor maxTokens must be at least 1024")
}
}
}
impl From<AdvisorBudget> for u64 {
fn from(value: AdvisorBudget) -> Self {
value.0
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all(deserialize = "camelCase", serialize = "snake_case"))]
pub struct AdvisorOptions {
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_uses: Option<serde_json::Number>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<AdvisorBudget>,
#[serde(skip_serializing_if = "Option::is_none")]
pub caching: Option<CacheDirective>,
}
impl HostedTool {
pub fn from_definition(id: &str, args: &Value) -> Result<Option<Self>> {
fn parse<T: serde::de::DeserializeOwned>(args: &Value) -> Result<T> {
serde_json::from_value(args.clone()).map_err(|error| Error::InvalidArgument {
field: error.to_string(),
value: args.clone(),
})
}
let tool = match id.strip_prefix("anthropic.").unwrap_or(id) {
"code_execution_20250522" => Self::CodeExecution(CodeVersion::V20250522),
"code_execution_20250825" => Self::CodeExecution(CodeVersion::V20250825),
"code_execution_20260120" => Self::CodeExecution(CodeVersion::V20260120),
"computer_20241022" => Self::Computer(ComputerVersion::V20241022, parse(args)?),
"computer_20250124" => Self::Computer(ComputerVersion::V20250124, parse(args)?),
"computer_20251124" => Self::Computer(ComputerVersion::V20251124, parse(args)?),
"text_editor_20241022" => {
Self::Editor(EditorVersion::V20241022, EditorOptions::default())
}
"text_editor_20250124" => {
Self::Editor(EditorVersion::V20250124, EditorOptions::default())
}
"text_editor_20250429" => {
Self::Editor(EditorVersion::V20250429, EditorOptions::default())
}
"text_editor_20250728" => Self::Editor(EditorVersion::V20250728, parse(args)?),
"bash_20241022" => Self::Bash(BashVersion::V20241022),
"bash_20250124" => Self::Bash(BashVersion::V20250124),
"computer_toolset_20260801" => {
#[derive(Deserialize)]
struct Options {
configs: Option<BTreeMap<ComputerAction, MemberOptions>>,
}
Self::ComputerToolset(parse::<Options>(args)?.configs.unwrap_or_default())
}
"memory_20250818" => Self::Memory,
"web_fetch_20250910" => Self::WebFetch(FetchVersion::V20250910, parse(args)?),
"web_fetch_20260209" => Self::WebFetch(FetchVersion::V20260209, parse(args)?),
"web_fetch_20260318" => Self::WebFetch(FetchVersion::V20260318, parse(args)?),
"web_search_20250305" => Self::WebSearch(SearchVersion::V20250305, parse(args)?),
"web_search_20260209" => Self::WebSearch(SearchVersion::V20260209, parse(args)?),
"web_search_20260318" => Self::WebSearch(SearchVersion::V20260318, parse(args)?),
"tool_search_regex_20251119" => Self::ToolSearch(SearchAlgorithm::Regex),
"tool_search_bm25_20251119" => Self::ToolSearch(SearchAlgorithm::Bm25),
"advisor_20260301" => Self::Advisor(parse(args)?),
_ => return Ok(None),
};
Ok(Some(tool))
}
fn descriptor(&self) -> (&'static str, Option<&'static str>, Option<Feature>) {
use Feature::*;
match self {
Self::CodeExecution(version) => match version {
CodeVersion::V20250522 => (
"code_execution_20250522",
Some("code_execution"),
Some(CodeExecution20250522),
),
CodeVersion::V20250825 => (
"code_execution_20250825",
Some("code_execution"),
Some(CodeExecution20250825),
),
CodeVersion::V20260120 => ("code_execution_20260120", Some("code_execution"), None),
},
Self::Computer(version, _) => match version {
ComputerVersion::V20241022 => (
"computer_20241022",
Some("computer"),
Some(Computer20241022),
),
ComputerVersion::V20250124 => (
"computer_20250124",
Some("computer"),
Some(Computer20250124),
),
ComputerVersion::V20251124 => (
"computer_20251124",
Some("computer"),
Some(Computer20251124),
),
},
Self::Editor(version, _) => match version {
EditorVersion::V20241022 => (
"text_editor_20241022",
Some("str_replace_editor"),
Some(Computer20241022),
),
EditorVersion::V20250124 => (
"text_editor_20250124",
Some("str_replace_editor"),
Some(Computer20250124),
),
EditorVersion::V20250429 => (
"text_editor_20250429",
Some("str_replace_based_edit_tool"),
Some(Computer20250124),
),
EditorVersion::V20250728 => (
"text_editor_20250728",
Some("str_replace_based_edit_tool"),
None,
),
},
Self::Bash(BashVersion::V20241022) => {
("bash_20241022", Some("bash"), Some(Computer20241022))
}
Self::Bash(BashVersion::V20250124) => {
("bash_20250124", Some("bash"), Some(Computer20250124))
}
Self::ComputerToolset(_) => ("computer_toolset_20260801", None, None),
Self::Memory => ("memory_20250818", Some("memory"), Some(ContextManagement)),
Self::WebFetch(version, _) => match version {
FetchVersion::V20250910 => {
("web_fetch_20250910", Some("web_fetch"), Some(WebFetch))
}
FetchVersion::V20260209 => (
"web_fetch_20260209",
Some("web_fetch"),
Some(CodeExecutionWebTools),
),
FetchVersion::V20260318 => ("web_fetch_20260318", Some("web_fetch"), None),
},
Self::WebSearch(version, _) => match version {
SearchVersion::V20250305 => ("web_search_20250305", Some("web_search"), None),
SearchVersion::V20260209 => (
"web_search_20260209",
Some("web_search"),
Some(CodeExecutionWebTools),
),
SearchVersion::V20260318 => ("web_search_20260318", Some("web_search"), None),
},
Self::ToolSearch(SearchAlgorithm::Regex) => (
"tool_search_tool_regex_20251119",
Some("tool_search_tool_regex"),
None,
),
Self::ToolSearch(SearchAlgorithm::Bm25) => (
"tool_search_tool_bm25_20251119",
Some("tool_search_tool_bm25"),
None,
),
Self::Advisor(_) => ("advisor_20260301", Some("advisor"), Some(Advisor)),
}
}
pub fn name(&self) -> Option<&'static str> {
self.descriptor().1
}
fn feature(&self) -> Option<Feature> {
self.descriptor().2
}
pub fn wire(&self) -> Value {
fn serialize(value: &impl Serialize) -> Value {
serde_json::to_value(value).expect("provider options are JSON serializable")
}
let mut wire = match self {
Self::Computer(version, options) => {
let mut value = serialize(options);
if !matches!(version, ComputerVersion::V20251124) {
value.as_object_mut().unwrap().remove("enable_zoom");
}
value
}
Self::Editor(EditorVersion::V20250728, options) => serialize(options),
Self::ComputerToolset(configs) if !configs.is_empty() => json!({"configs":configs}),
Self::WebFetch(version, options) => {
let mut value = serialize(options);
if !matches!(version, FetchVersion::V20260318) {
value.as_object_mut().unwrap().remove("use_cache");
value.as_object_mut().unwrap().remove("response_inclusion");
}
value
}
Self::WebSearch(version, options) => {
let mut value = serialize(options);
if !matches!(version, SearchVersion::V20260318) {
value.as_object_mut().unwrap().remove("response_inclusion");
}
value
}
Self::Advisor(options) => serialize(options),
Self::CodeExecution(_)
| Self::Editor(_, _)
| Self::Bash(_)
| Self::ComputerToolset(_)
| Self::Memory
| Self::ToolSearch(_) => json!({}),
};
let (kind, name, _) = self.descriptor();
wire["type"] = json!(kind);
if let Some(name) = name {
wire["name"] = json!(name);
}
wire
}
}