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