1use std::collections::BTreeMap;
2
3use schemars::{JsonSchema, schema_for};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::{ExtensionMap, Message};
8
9const PROVIDER_TOOLS_METADATA_KEY: &str = "runifold.request.provider_tools.v1";
10const RESPONSE_MODE_METADATA_KEY: &str = "runifold.request.response_mode.v1";
11
12#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
14pub struct ModelRef {
15 pub provider: String,
17 pub name: String,
19}
20
21impl ModelRef {
22 pub fn new(provider: impl Into<String>, name: impl Into<String>) -> Self {
24 Self {
25 provider: provider.into(),
26 name: name.into(),
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "snake_case")]
34#[non_exhaustive]
35pub enum FeaturePolicy {
36 #[default]
38 Strict,
39 AllowEmulation,
41 BestEffort,
43}
44
45#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
47pub struct GenerationOptions {
48 pub temperature: Option<f64>,
50 pub top_p: Option<f64>,
52 pub max_output_tokens: Option<u64>,
54 pub seed: Option<u64>,
56 pub stop: Vec<String>,
58}
59
60#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
62#[serde(rename_all = "snake_case")]
63#[non_exhaustive]
64pub enum ResponseMode {
65 #[default]
67 Streaming,
68 Complete,
70}
71
72#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
74#[serde(tag = "type", rename_all = "snake_case")]
75#[non_exhaustive]
76pub enum OutputFormat {
77 #[default]
79 Text,
80 Json,
82 JsonSchema {
84 name: String,
86 schema: Value,
88 strict: bool,
90 },
91}
92
93impl OutputFormat {
94 pub fn typed<T>(name: impl Into<String>) -> Self
99 where
100 T: JsonSchema,
101 {
102 Self::JsonSchema {
103 name: name.into(),
104 schema: schema_for!(T).to_value(),
105 strict: true,
106 }
107 }
108
109 pub fn typed_with_strictness<T>(name: impl Into<String>, strict: bool) -> Self
112 where
113 T: JsonSchema,
114 {
115 Self::JsonSchema {
116 name: name.into(),
117 schema: schema_for!(T).to_value(),
118 strict,
119 }
120 }
121}
122
123#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
125pub struct ToolSpec {
126 pub name: String,
128 pub description: String,
130 pub input_schema: Value,
132 pub output_schema: Option<Value>,
134 pub metadata: ExtensionMap,
136}
137
138#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
144pub struct ProviderToolSpec {
145 pub provider: String,
147 pub tool_type: String,
149 pub options: BTreeMap<String, Value>,
151}
152
153impl ProviderToolSpec {
154 pub fn new(
161 provider: impl Into<String>,
162 tool_type: impl Into<String>,
163 ) -> Result<Self, crate::ModelError> {
164 let provider = provider.into();
165 let tool_type = tool_type.into();
166 if !is_provider_token(&provider) {
167 return Err(crate::ModelError::local(
168 crate::ModelErrorKind::InvalidRequest,
169 "provider-native tool provider must be a non-empty ASCII token",
170 ));
171 }
172 if !is_provider_token(&tool_type) {
173 return Err(crate::ModelError::local(
174 crate::ModelErrorKind::InvalidRequest,
175 "provider-native tool type must be a non-empty ASCII token",
176 ));
177 }
178 Ok(Self {
179 provider,
180 tool_type,
181 options: BTreeMap::new(),
182 })
183 }
184
185 #[must_use]
187 pub fn option(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
188 self.options.insert(name.into(), value.into());
189 self
190 }
191}
192
193fn is_provider_token(value: &str) -> bool {
194 !value.is_empty()
195 && value.len() <= 128
196 && value
197 .bytes()
198 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
199}
200
201#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
203#[serde(tag = "type", rename_all = "snake_case")]
204#[non_exhaustive]
205pub enum ToolChoice {
206 #[default]
208 Auto,
209 None,
211 Required,
213 Named {
215 name: String,
217 },
218}
219
220#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
222pub struct ModelRequest {
223 pub model: ModelRef,
225 pub messages: Vec<Message>,
227 pub tools: Vec<ToolSpec>,
229 pub tool_choice: ToolChoice,
231 pub output_format: OutputFormat,
233 pub generation: GenerationOptions,
235 pub feature_policy: FeaturePolicy,
237 pub provider_options: BTreeMap<String, Value>,
239 pub metadata: ExtensionMap,
241}
242
243impl ModelRequest {
244 pub fn new(model: ModelRef, message: Message) -> Self {
246 Self {
247 model,
248 messages: vec![message],
249 tools: Vec::new(),
250 tool_choice: ToolChoice::Auto,
251 output_format: OutputFormat::Text,
252 generation: GenerationOptions::default(),
253 feature_policy: FeaturePolicy::Strict,
254 provider_options: BTreeMap::new(),
255 metadata: BTreeMap::new(),
256 }
257 }
258
259 #[must_use]
261 pub fn message(mut self, message: Message) -> Self {
262 self.messages.push(message);
263 self
264 }
265
266 #[must_use]
268 pub fn tool(mut self, tool: ToolSpec) -> Self {
269 self.tools.push(tool);
270 self
271 }
272
273 #[must_use]
275 pub fn provider_tool(mut self, tool: ProviderToolSpec) -> Self {
276 let mut tools = self.provider_tools();
277 tools.push(tool);
278 self.metadata.insert(
279 PROVIDER_TOOLS_METADATA_KEY.into(),
280 Value::Array(tools.into_iter().map(provider_tool_value).collect()),
281 );
282 self
283 }
284
285 #[must_use]
287 pub fn provider_tools(&self) -> Vec<ProviderToolSpec> {
288 self.metadata
289 .get(PROVIDER_TOOLS_METADATA_KEY)
290 .and_then(|value| serde_json::from_value(value.clone()).ok())
291 .unwrap_or_default()
292 }
293
294 #[must_use]
296 pub fn generation(mut self, generation: GenerationOptions) -> Self {
297 self.generation = generation;
298 self
299 }
300
301 #[must_use]
303 pub fn response_mode(mut self, response_mode: ResponseMode) -> Self {
304 let value = match response_mode {
305 ResponseMode::Streaming => "streaming",
306 ResponseMode::Complete => "complete",
307 };
308 self.metadata.insert(
309 RESPONSE_MODE_METADATA_KEY.into(),
310 Value::String(value.into()),
311 );
312 self
313 }
314
315 #[must_use]
317 pub fn selected_response_mode(&self) -> ResponseMode {
318 match self
319 .metadata
320 .get(RESPONSE_MODE_METADATA_KEY)
321 .and_then(Value::as_str)
322 {
323 Some("complete") => ResponseMode::Complete,
324 _ => ResponseMode::Streaming,
325 }
326 }
327
328 #[must_use]
330 pub fn provider_option(mut self, provider: impl Into<String>, options: Value) -> Self {
331 self.provider_options.insert(provider.into(), options);
332 self
333 }
334
335 #[must_use]
337 pub fn output_format(mut self, output_format: OutputFormat) -> Self {
338 self.output_format = output_format;
339 self
340 }
341
342 #[must_use]
344 pub fn structured_output<T>(self, name: impl Into<String>) -> Self
345 where
346 T: JsonSchema,
347 {
348 self.output_format(OutputFormat::typed::<T>(name))
349 }
350
351 #[must_use]
353 pub const fn feature_policy(mut self, feature_policy: FeaturePolicy) -> Self {
354 self.feature_policy = feature_policy;
355 self
356 }
357}
358
359fn provider_tool_value(tool: ProviderToolSpec) -> Value {
360 Value::Object(
361 [
362 ("provider".into(), Value::String(tool.provider)),
363 ("tool_type".into(), Value::String(tool.tool_type)),
364 (
365 "options".into(),
366 Value::Object(tool.options.into_iter().collect()),
367 ),
368 ]
369 .into_iter()
370 .collect(),
371 )
372}