objectiveai_sdk/agent/openrouter/
agent.rs1use indexmap::IndexMap;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use twox_hash::XxHash3_128;
7
8#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
10#[schemars(rename = "agent.openrouter.AgentBase")]
11pub struct AgentBase {
12 pub upstream: super::Upstream,
14
15 pub model: String,
17
18 #[serde(default)]
20 pub output_mode: super::OutputMode,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
26 #[schemars(extend("omitempty" = true))]
27 pub synthetic_reasoning: Option<bool>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
33 #[schemars(extend("omitempty" = true))]
34 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_u64)]
35 pub top_logprobs: Option<u64>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 #[schemars(extend("omitempty" = true))]
40 pub prefix_messages: Option<Vec<super::super::completions::message::Message>>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 #[schemars(extend("omitempty" = true))]
45 pub post_system_prefix_messages: Option<Vec<super::super::completions::message::Message>>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 #[schemars(extend("omitempty" = true))]
50 pub suffix_messages: Option<Vec<super::super::completions::message::Message>>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 #[schemars(extend("omitempty" = true))]
55 pub mcp_servers: Option<super::super::McpServers>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
60 #[schemars(extend("omitempty" = true))]
61 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_f64)]
62 pub frequency_penalty: Option<f64>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 #[schemars(extend("omitempty" = true))]
66 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_indexmap_string_i64)]
67 pub logit_bias: Option<IndexMap<String, i64>>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 #[schemars(extend("omitempty" = true))]
71 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_u64)]
72 pub max_completion_tokens: Option<u64>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 #[schemars(extend("omitempty" = true))]
76 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_f64)]
77 pub presence_penalty: Option<f64>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 #[schemars(extend("omitempty" = true))]
81 pub stop: Option<super::Stop>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 #[schemars(extend("omitempty" = true))]
85 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_f64)]
86 pub temperature: Option<f64>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 #[schemars(extend("omitempty" = true))]
90 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_f64)]
91 pub top_p: Option<f64>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
96 #[schemars(extend("omitempty" = true))]
97 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_u64)]
98 pub max_tokens: Option<u64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 #[schemars(extend("omitempty" = true))]
102 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_f64)]
103 pub min_p: Option<f64>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 #[schemars(extend("omitempty" = true))]
107 pub provider: Option<super::Provider>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 #[schemars(extend("omitempty" = true))]
111 pub reasoning: Option<super::Reasoning>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 #[schemars(extend("omitempty" = true))]
115 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_f64)]
116 pub repetition_penalty: Option<f64>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 #[schemars(extend("omitempty" = true))]
120 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_f64)]
121 pub top_a: Option<f64>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 #[schemars(extend("omitempty" = true))]
125 #[arbitrary(with = crate::arbitrary_util::arbitrary_option_u64)]
126 pub top_k: Option<u64>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 #[schemars(extend("omitempty" = true))]
130 pub verbosity: Option<super::Verbosity>,
131}
132
133impl AgentBase {
134 pub fn prepare(&mut self) {
136 self.synthetic_reasoning = match self.synthetic_reasoning {
137 Some(false) => None,
138 other => other,
139 };
140 self.top_logprobs = match self.top_logprobs {
141 Some(0) | Some(1) => None,
142 other => other,
143 };
144 self.prefix_messages = match self.prefix_messages.take() {
145 Some(prefix_messages) if prefix_messages.is_empty() => None,
146 Some(mut prefix_messages) => {
147 super::super::completions::message::prompt::prepare(&mut prefix_messages);
148 if prefix_messages.is_empty() { None } else { Some(prefix_messages) }
149 }
150 None => None,
151 };
152 self.post_system_prefix_messages = match self.post_system_prefix_messages.take() {
153 Some(msgs) if msgs.is_empty() => None,
154 Some(mut msgs) => {
155 super::super::completions::message::prompt::prepare(&mut msgs);
156 if msgs.is_empty() { None } else { Some(msgs) }
157 }
158 None => None,
159 };
160 self.suffix_messages = match self.suffix_messages.take() {
161 Some(suffix_messages) if suffix_messages.is_empty() => None,
162 Some(mut suffix_messages) => {
163 super::super::completions::message::prompt::prepare(&mut suffix_messages);
164 if suffix_messages.is_empty() { None } else { Some(suffix_messages) }
165 }
166 None => None,
167 };
168 self.mcp_servers = match self.mcp_servers.take() {
169 Some(mcp_servers) => super::super::mcp::mcp_servers::prepare(mcp_servers),
170 None => None,
171 };
172 self.frequency_penalty = match self.frequency_penalty {
173 Some(frequency_penalty) if frequency_penalty == 0.0 => None,
174 other => other,
175 };
176 self.logit_bias = match self.logit_bias.take() {
177 Some(logit_bias) if logit_bias.is_empty() => None,
178 Some(mut logit_bias) => {
179 logit_bias.retain(|_, &mut weight| weight != 0);
180 logit_bias.sort_unstable_keys();
181 Some(logit_bias)
182 }
183 None => None,
184 };
185 self.max_completion_tokens = match self.max_completion_tokens {
186 Some(0) => None,
187 other => other,
188 };
189 self.presence_penalty = match self.presence_penalty {
190 Some(presence_penalty) if presence_penalty == 0.0 => None,
191 other => other,
192 };
193 self.stop = match self.stop.take() {
194 Some(stop) => stop.prepare(),
195 None => None,
196 };
197 self.temperature = match self.temperature {
198 Some(temperature) if temperature == 1.0 => None,
199 other => other,
200 };
201 self.top_p = match self.top_p {
202 Some(top_p) if top_p == 1.0 => None,
203 other => other,
204 };
205 self.max_tokens = match self.max_tokens {
206 Some(0) => None,
207 other => other,
208 };
209 self.min_p = match self.min_p {
210 Some(min_p) if min_p == 0.0 => None,
211 other => other,
212 };
213 self.provider = match self.provider.take() {
214 Some(provider) => provider.prepare(),
215 None => None,
216 };
217 self.reasoning = match self.reasoning.take() {
218 Some(reasoning) => reasoning.prepare(),
219 None => None,
220 };
221 self.repetition_penalty = match self.repetition_penalty {
222 Some(repetition_penalty) if repetition_penalty == 1.0 => None,
223 other => other,
224 };
225 self.top_a = match self.top_a {
226 Some(top_a) if top_a == 0.0 => None,
227 other => other,
228 };
229 self.top_k = match self.top_k {
230 Some(0) => None,
231 other => other,
232 };
233 self.verbosity = match self.verbosity.take() {
234 Some(verbosity) => verbosity.prepare(),
235 None => None,
236 };
237 }
238
239 pub fn validate(&self) -> Result<(), String> {
241 fn validate_f64(
242 name: &str,
243 value: Option<f64>,
244 min: f64,
245 max: f64,
246 ) -> Result<(), String> {
247 if let Some(v) = value {
248 if !v.is_finite() {
249 return Err(format!("`{}` must be a finite number", name));
250 }
251 if v < min || v > max {
252 return Err(format!(
253 "`{}` must be between {} and {}",
254 name, min, max
255 ));
256 }
257 }
258 Ok(())
259 }
260 fn validate_u64(
261 name: &str,
262 value: Option<u64>,
263 min: u64,
264 max: u64,
265 ) -> Result<(), String> {
266 if let Some(v) = value {
267 if v < min || v > max {
268 return Err(format!(
269 "`{}` must be between {} and {}",
270 name, min, max
271 ));
272 }
273 }
274 Ok(())
275 }
276 if self.model.is_empty() {
277 return Err("`model` string cannot be empty".to_string());
278 }
279 if self.synthetic_reasoning.is_some()
280 && let super::OutputMode::Instruction = self.output_mode
281 {
282 return Err(
283 "`synthetic_reasoning` cannot be true when `output_mode` is \"instruction\""
284 .to_string(),
285 );
286 }
287 if let Some(top_logprobs) = self.top_logprobs
288 && top_logprobs > 20
289 {
290 return Err("`top_logprobs` must be at most 20".to_string());
291 }
292 if let Some(mcp_servers) = &self.mcp_servers {
293 super::super::mcp::mcp_servers::validate(mcp_servers)?;
294 }
295 validate_f64("frequency_penalty", self.frequency_penalty, -2.0, 2.0)?;
296 if let Some(logit_bias) = &self.logit_bias {
297 for (token, weight) in logit_bias {
298 if token.is_empty() {
299 return Err("`logit_bias` keys cannot be empty".to_string());
300 } else if !token.chars().all(|c| c.is_ascii_digit()) {
301 return Err(
302 "`logit_bias` keys must be stringified token IDs"
303 .to_string(),
304 );
305 } else if token.chars().next().unwrap() == '0'
306 && token.len() > 1
307 {
308 return Err("`logit_bias` keys cannot have leading zeros"
309 .to_string());
310 } else if *weight < -100 || *weight > 100 {
311 return Err(
312 "`logit_bias` values must be between -100 and 100"
313 .to_string(),
314 );
315 }
316 }
317 }
318 validate_u64(
319 "max_completion_tokens",
320 self.max_completion_tokens,
321 0,
322 i32::MAX as u64,
323 )?;
324 validate_f64("presence_penalty", self.presence_penalty, -2.0, 2.0)?;
325 if let Some(stop) = &self.stop {
326 stop.validate()?;
327 }
328 validate_f64("temperature", self.temperature, 0.0, 2.0)?;
329 validate_f64("top_p", self.top_p, 0.0, 1.0)?;
330 validate_u64("max_tokens", self.max_tokens, 0, i32::MAX as u64)?;
331 validate_f64("min_p", self.min_p, 0.0, 1.0)?;
332 if let Some(provider) = &self.provider {
333 provider.validate()?;
334 }
335 if let Some(reasoning) = &self.reasoning {
336 reasoning.validate()?;
337 }
338 validate_f64("repetition_penalty", self.repetition_penalty, 0.0, 2.0)?;
339 validate_f64("top_a", self.top_a, 0.0, 1.0)?;
340 validate_u64("top_k", self.top_k, 0, i32::MAX as u64)?;
341 if let Some(verbosity) = &self.verbosity {
342 verbosity.validate()?;
343 }
344 Ok(())
345 }
346
347 pub fn merged_messages(
349 &self,
350 messages: Vec<super::super::completions::message::Message>,
351 ) -> Vec<super::super::completions::message::Message> {
352 use super::super::completions::message::Message;
353 let prefix_len = self.prefix_messages.as_ref().map_or(0, |m| m.len());
354 let post_sys_len = self.post_system_prefix_messages.as_ref().map_or(0, |m| m.len());
355 let suffix_len = self.suffix_messages.as_ref().map_or(0, |m| m.len());
356 let mut merged = Vec::with_capacity(prefix_len + post_sys_len + messages.len() + suffix_len);
357 if let Some(prefix) = &self.prefix_messages {
358 merged.extend(prefix.iter().cloned());
359 }
360 let mut post_sys_inserted = self.post_system_prefix_messages.is_none();
361 for msg in messages {
362 if !post_sys_inserted {
363 if !matches!(msg, Message::System(_) | Message::Developer(_)) {
364 merged.extend(self.post_system_prefix_messages.as_ref().unwrap().iter().cloned());
365 post_sys_inserted = true;
366 }
367 }
368 merged.push(msg);
369 }
370 if !post_sys_inserted {
371 merged.extend(self.post_system_prefix_messages.as_ref().unwrap().iter().cloned());
372 }
373 if let Some(suffix) = &self.suffix_messages {
374 merged.extend(suffix.iter().cloned());
375 }
376 merged
377 }
378
379 pub fn id(&self) -> String {
381 let mut hasher = XxHash3_128::with_seed(0);
382 hasher.write(serde_json::to_string(self).unwrap().as_bytes());
383 format!("{:0>22}", base62::encode(hasher.finish_128()))
384 }
385}
386
387#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
389#[schemars(rename = "agent.openrouter.Agent")]
390pub struct Agent {
391 pub id: String,
393 #[serde(flatten)]
395 pub base: AgentBase,
396}
397
398impl TryFrom<AgentBase> for Agent {
399 type Error = String;
400 fn try_from(mut base: AgentBase) -> Result<Self, Self::Error> {
401 base.prepare();
402 base.validate()?;
403 let id = base.id();
404 Ok(Agent { id, base })
405 }
406}