use std::collections::BTreeMap;
use schemars::{JsonSchema, schema_for};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{ExtensionMap, Message};
const PROVIDER_TOOLS_METADATA_KEY: &str = "runifold.request.provider_tools.v1";
const RESPONSE_MODE_METADATA_KEY: &str = "runifold.request.response_mode.v1";
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ModelRef {
pub provider: String,
pub name: String,
}
impl ModelRef {
pub fn new(provider: impl Into<String>, name: impl Into<String>) -> Self {
Self {
provider: provider.into(),
name: name.into(),
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum FeaturePolicy {
#[default]
Strict,
AllowEmulation,
BestEffort,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct GenerationOptions {
pub temperature: Option<f64>,
pub top_p: Option<f64>,
pub max_output_tokens: Option<u64>,
pub seed: Option<u64>,
pub stop: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseMode {
#[default]
Streaming,
Complete,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum OutputFormat {
#[default]
Text,
Json,
JsonSchema {
name: String,
schema: Value,
strict: bool,
},
}
impl OutputFormat {
pub fn typed<T>(name: impl Into<String>) -> Self
where
T: JsonSchema,
{
Self::JsonSchema {
name: name.into(),
schema: schema_for!(T).to_value(),
strict: true,
}
}
pub fn typed_with_strictness<T>(name: impl Into<String>, strict: bool) -> Self
where
T: JsonSchema,
{
Self::JsonSchema {
name: name.into(),
schema: schema_for!(T).to_value(),
strict,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ToolSpec {
pub name: String,
pub description: String,
pub input_schema: Value,
pub output_schema: Option<Value>,
pub metadata: ExtensionMap,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ProviderToolSpec {
pub provider: String,
pub tool_type: String,
pub options: BTreeMap<String, Value>,
}
impl ProviderToolSpec {
pub fn new(
provider: impl Into<String>,
tool_type: impl Into<String>,
) -> Result<Self, crate::ModelError> {
let provider = provider.into();
let tool_type = tool_type.into();
if !is_provider_token(&provider) {
return Err(crate::ModelError::local(
crate::ModelErrorKind::InvalidRequest,
"provider-native tool provider must be a non-empty ASCII token",
));
}
if !is_provider_token(&tool_type) {
return Err(crate::ModelError::local(
crate::ModelErrorKind::InvalidRequest,
"provider-native tool type must be a non-empty ASCII token",
));
}
Ok(Self {
provider,
tool_type,
options: BTreeMap::new(),
})
}
#[must_use]
pub fn option(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.options.insert(name.into(), value.into());
self
}
}
fn is_provider_token(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ToolChoice {
#[default]
Auto,
None,
Required,
Named {
name: String,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ModelRequest {
pub model: ModelRef,
pub messages: Vec<Message>,
pub tools: Vec<ToolSpec>,
pub tool_choice: ToolChoice,
pub output_format: OutputFormat,
pub generation: GenerationOptions,
pub feature_policy: FeaturePolicy,
pub provider_options: BTreeMap<String, Value>,
pub metadata: ExtensionMap,
}
impl ModelRequest {
pub fn new(model: ModelRef, message: Message) -> Self {
Self {
model,
messages: vec![message],
tools: Vec::new(),
tool_choice: ToolChoice::Auto,
output_format: OutputFormat::Text,
generation: GenerationOptions::default(),
feature_policy: FeaturePolicy::Strict,
provider_options: BTreeMap::new(),
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn message(mut self, message: Message) -> Self {
self.messages.push(message);
self
}
#[must_use]
pub fn tool(mut self, tool: ToolSpec) -> Self {
self.tools.push(tool);
self
}
#[must_use]
pub fn provider_tool(mut self, tool: ProviderToolSpec) -> Self {
let mut tools = self.provider_tools();
tools.push(tool);
self.metadata.insert(
PROVIDER_TOOLS_METADATA_KEY.into(),
Value::Array(tools.into_iter().map(provider_tool_value).collect()),
);
self
}
#[must_use]
pub fn provider_tools(&self) -> Vec<ProviderToolSpec> {
self.metadata
.get(PROVIDER_TOOLS_METADATA_KEY)
.and_then(|value| serde_json::from_value(value.clone()).ok())
.unwrap_or_default()
}
#[must_use]
pub fn generation(mut self, generation: GenerationOptions) -> Self {
self.generation = generation;
self
}
#[must_use]
pub fn response_mode(mut self, response_mode: ResponseMode) -> Self {
let value = match response_mode {
ResponseMode::Streaming => "streaming",
ResponseMode::Complete => "complete",
};
self.metadata.insert(
RESPONSE_MODE_METADATA_KEY.into(),
Value::String(value.into()),
);
self
}
#[must_use]
pub fn selected_response_mode(&self) -> ResponseMode {
match self
.metadata
.get(RESPONSE_MODE_METADATA_KEY)
.and_then(Value::as_str)
{
Some("complete") => ResponseMode::Complete,
_ => ResponseMode::Streaming,
}
}
#[must_use]
pub fn provider_option(mut self, provider: impl Into<String>, options: Value) -> Self {
self.provider_options.insert(provider.into(), options);
self
}
#[must_use]
pub fn output_format(mut self, output_format: OutputFormat) -> Self {
self.output_format = output_format;
self
}
#[must_use]
pub fn structured_output<T>(self, name: impl Into<String>) -> Self
where
T: JsonSchema,
{
self.output_format(OutputFormat::typed::<T>(name))
}
#[must_use]
pub const fn feature_policy(mut self, feature_policy: FeaturePolicy) -> Self {
self.feature_policy = feature_policy;
self
}
}
fn provider_tool_value(tool: ProviderToolSpec) -> Value {
Value::Object(
[
("provider".into(), Value::String(tool.provider)),
("tool_type".into(), Value::String(tool.tool_type)),
(
"options".into(),
Value::Object(tool.options.into_iter().collect()),
),
]
.into_iter()
.collect(),
)
}